From 0bd4f09b07192af852395c33b87f41992eaa2a9c Mon Sep 17 00:00:00 2001 From: Tom Date: Tue, 27 Oct 2015 13:00:58 +0000 Subject: [PATCH] Initial commit --- .gitignore | 108 ++++++++++++++++++++++ cvsslib/__init__.py | 48 ++++++++++ cvsslib/cvss2/__init__.py | 5 ++ cvsslib/cvss2/enums.py | 102 +++++++++++++++++++++ cvsslib/cvss3/__init__.py | 3 + cvsslib/cvss3/calculations.py | 128 ++++++++++++++++++++++++++ cvsslib/cvss3/enums.py | 163 ++++++++++++++++++++++++++++++++++ cvsslib/mixin.py | 75 ++++++++++++++++ cvsslib/vector.py | 60 +++++++++++++ setup.py | 12 +++ 10 files changed, 704 insertions(+) create mode 100644 .gitignore create mode 100644 cvsslib/__init__.py create mode 100644 cvsslib/cvss2/__init__.py create mode 100644 cvsslib/cvss2/enums.py create mode 100644 cvsslib/cvss3/__init__.py create mode 100644 cvsslib/cvss3/calculations.py create mode 100644 cvsslib/cvss3/enums.py create mode 100644 cvsslib/mixin.py create mode 100644 cvsslib/vector.py create mode 100644 setup.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..30b0dd9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,108 @@ +# Created by .ignore support plugin (hsz.mobi) +### Python template +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*,cover + +# Translations +*.mo +*.pot + +# Django stuff: +*.log + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ +### JetBrains template +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio + +*.iml + +## Directory-based project format: +.idea/ +# if you remove the above rule, at least ignore the following: + +# User-specific stuff: +# .idea/workspace.xml +# .idea/tasks.xml +# .idea/dictionaries + +# Sensitive or high-churn files: +# .idea/dataSources.ids +# .idea/dataSources.xml +# .idea/sqlDataSources.xml +# .idea/dynamic.xml +# .idea/uiDesigner.xml + +# Gradle: +# .idea/gradle.xml +# .idea/libraries + +# Mongo Explorer plugin: +# .idea/mongoSettings.xml + +## File-based project format: +*.ipr +*.iws + +## Plugin-specific files: + +# IntelliJ +/out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties + diff --git a/cvsslib/__init__.py b/cvsslib/__init__.py new file mode 100644 index 0000000..5b95932 --- /dev/null +++ b/cvsslib/__init__.py @@ -0,0 +1,48 @@ +import enum + + +def make_display_name(str): + return " ".join( + s.capitalize() for s in str.lower().split("_") + ) + + +class BaseEnum(enum.Enum): + @classmethod + def get_value_from_vector(cls, key): + key = key.lower() + + for name, value in cls.__members__.items(): + if name[0].lower() == key: + return value + + if key == "x" and hasattr(cls, "NOT_DEFINED"): + return cls.NOT_DEFINED + + raise RuntimeError("Unknown vector key {0} for {1}".format(key, cls)) + + @classmethod + def choices(cls): + return [(value.value, make_display_name(name)) for name, value in cls.__members__.items()] + + @classmethod + def extend(cls, name, extra, doc=""): + cls = enum.Enum( + value=name, + names=cls.to_mapping(extra), + type=BaseEnum + ) + cls.__doc__ = doc + return cls + + @classmethod + def to_mapping(cls, extra=None): + returner = { + name: value.value + for name, value in cls.__members__.items() + } + + if extra: + returner.update(extra) + + return returner \ No newline at end of file diff --git a/cvsslib/cvss2/__init__.py b/cvsslib/cvss2/__init__.py new file mode 100644 index 0000000..8f21927 --- /dev/null +++ b/cvsslib/cvss2/__init__.py @@ -0,0 +1,5 @@ +from . import enums + + +def calculate(): + pass diff --git a/cvsslib/cvss2/enums.py b/cvsslib/cvss2/enums.py new file mode 100644 index 0000000..050e870 --- /dev/null +++ b/cvsslib/cvss2/enums.py @@ -0,0 +1,102 @@ +from .. import BaseEnum + + +# Taken from https://www.first.org/cvss/v2/guide#i3.2.1 + +class AccessVector(BaseEnum): + LOCAL_ACCESS = 0.395 + ADJACENT_NETWORK = 0.646 + NETWORK_ACCESSIBLE = 1 + + +class AccessComplexity(BaseEnum): + HIGH = 0.35 + MEDIUM = 0.61 + LOW = 0.71 + + +class Authentication(BaseEnum): + MULTIPLE = 0.45 + SINGLE = 0.56 + NONE = 0.704 + + +class ConfidentialityImpact(BaseEnum): + NONE = 0 + PARTIAL = 0.275 + COMPLETE = 0.660 + + +class IntegrityImpact(BaseEnum): + NONE = 0 + PARTIAL = 0.275 + COMPLETE = 0.660 + + +class AvailabilityImpact(BaseEnum): + NONE = 0 + PARTIAL = 0.275 + COMPLETE = 0.660 + + +# Temporal: +class Exploitability(BaseEnum): + UNPROVEN = 0.85 + PROOF_OF_CONCEPT = 0.9 + FUNCTIONAL = 0.95 + HIGH = 1 + NOT_DEFINED = 1 + + +class RemediationLevel(BaseEnum): + OFFICIAL_FIX = 0.87 + TEMPORARY_FIX = 0.90 + WORKAROUND = 0.95 + UNAVAILABLE = 1 + NOT_DEFINED = 1 + + +class ReportConfidence(BaseEnum): + UNCONFIRMED = 0.9 + UNCORROBORATED = 0.95 + CONFIRMED = 1 + NOT_DEFINED = 1 + + +# Environmental +class CollateralDamagePotential(BaseEnum): + NONE = 0 + LOW = 0.1 + LOW_MEDIUM = 0.3 + MEDIUM_HIGH = 0.4 + HIGH = 0.5 + NOT_DEFINED = 0 + + +class TargetDistribution(BaseEnum): + NONE = 0 + LOW = 0.25 + MEDIUM = 0.75 + HIGH = 1 + NOT_DEFINED = 1 + + +class ConfidentialityRequirement(BaseEnum): + LOW = 0.5 + MEDIUM = 1 + HIGH = 1.51 + NOT_DEFINED = 1 + + +class IntegrityRequirement(BaseEnum): + LOW = 0.5 + MEDIUM = 1 + HIGH = 1.51 + NOT_DEFINED = 1 + + +class AvailabilityRequirement(BaseEnum): + LOW = 0.5 + MEDIUM = 1.0 + HIGH = 1.51 + NOT_DEFINED = 1.0 diff --git a/cvsslib/cvss3/__init__.py b/cvsslib/cvss3/__init__.py new file mode 100644 index 0000000..ba271b8 --- /dev/null +++ b/cvsslib/cvss3/__init__.py @@ -0,0 +1,3 @@ +from .enums import * +from .calculations import * + diff --git a/cvsslib/cvss3/calculations.py b/cvsslib/cvss3/calculations.py new file mode 100644 index 0000000..8cefb23 --- /dev/null +++ b/cvsslib/cvss3/calculations.py @@ -0,0 +1,128 @@ +import math + +from .enums import * + + +def roundup(num): + return math.ceil(num * 10) / 10 + + +def calculate_exploitability_sub_score(attack_vector: AttackVector, + complexity: AttackComplexity, + privilege: PrivilegeRequired, + interaction: UserInteraction): + return 8.22 * attack_vector.value * complexity.value * privilege.value * interaction.value + + +def calculate_modified_exploitability_sub_score(vector: ModifiedAttackVector, + complexity: ModifiedAttackComplexity, + privilege: ModifiedPrivilegeRequired, + interaction: ModifiedUserInteraction): + return 8.22 * vector.value * complexity.value * privilege.value * interaction.value + + +def calculate_impact_sub_score(scope: Scope, + conf_impact: ConfidentialityImpact, + integ_impact: IntegrityImpact, + avail_impact: AvailabilityImpact): + base_impact_sub_score = 1 - ((1 - conf_impact.value) * (1 - integ_impact.value) * (1 - avail_impact.value)) + + if scope == scope.UNCHANGED: + return 6.42 * base_impact_sub_score + else: + # What they hell are people smoking... + return 7.52 * (base_impact_sub_score - 0.029) - 3.25 * math.pow(base_impact_sub_score - 0.02, 15) + + +def calculate_modified_impact_sub_score(scope: ModifiedScope, + modified_conf: ModifiedConfidentiality, + modified_integ: ModifiedIntegrity, + modified_avail: ModifiedAvailability, + conf_req: ConfidentialityRequirements, + integ_req: IntegrityRequirements, + avail_req: AvailabilityRequirements): + modified = min( + 1 - (1 - modified_conf.value * conf_req.value) * + (1 - modified_integ.value * integ_req.value) * + (1 - modified_avail.value * avail_req.value), + 0.915 + ) + + if scope == scope.UNCHANGED: + return 6.42 * modified + else: + return 7.52 * (modified - 0.029) - 3.25 * math.pow(modified - 0.02, 15) + + +def calculate_base_score(call, scope: Scope, privilege: PrivilegeRequired): + impact_sub_score = call(calculate_impact_sub_score) + + if impact_sub_score <= 0: + return 0 + else: + + override = {} + if scope.CHANGED: + # Ok, so the privilege enum needs slightly different values depending on the scope. God damn. + modified_privilege = privilege.extend("ModifiedPrivilegeRequired", {"LOW": 0.68, "HIGH": 0.50}) + privilege = getattr(modified_privilege, privilege.name) + override[PrivilegeRequired] = privilege + + exploitability_sub_score = call(calculate_exploitability_sub_score, override=override) + + combined_score = impact_sub_score + exploitability_sub_score + + if scope == Scope.CHANGED: + return roundup(min(1.08 * combined_score, 10)) + else: + return roundup(min(combined_score, 10)) + + +def calculate_temporal_score(base_score, + maturity: ExploitCodeMaturity, + remediation: RemediationLevel, + confidence: ReportConfidence): + return roundup(base_score * maturity.value * remediation.value * confidence.value) + + +def calculate_environmental_score(call, + modified_scope: ModifiedScope, + exploit_code: ExploitCodeMaturity, + remediation: RemediationLevel, + confidence: ReportConfidence, + privilege: ModifiedPrivilegeRequired): + modified_impact_sub_score = call(calculate_modified_impact_sub_score) + + if modified_impact_sub_score <= 0: + return 0 + + if modified_scope.CHANGED: + # Ok, so the privilege enum needs slightly different values depending on the scope. God damn. + modified_privilege = privilege.extend("ModifiedPrivilegeRequired", {"LOW": 0.68, "HIGH": 0.50}) + privilege = getattr(modified_privilege, privilege.name) + + modified_exploitability_sub_score = call(calculate_modified_exploitability_sub_score, + override={ModifiedPrivilegeRequired: privilege}) + + if modified_scope == modified_scope.UNCHANGED: + return roundup( + roundup(min(modified_impact_sub_score + modified_exploitability_sub_score, 10)) * + exploit_code.value * remediation.value * confidence.value + ) + else: + return roundup( + roundup(min(1.08 * (modified_impact_sub_score + modified_exploitability_sub_score), 10)) * + exploit_code.value * remediation.value * confidence.value + ) + + +def calculate(call): + base_score = call(calculate_base_score) + temporal_score = call(calculate_temporal_score, base_score) + environment_score = call(calculate_environmental_score) + + # impact_subscore = call(calculate_impact_sub_score) + # exploit_subscore = call(calculate_exploitability_sub_score) + # mod_impact_subscore = call(calculate_modified_impact_sub_score) + + return base_score, temporal_score, environment_score # , impact_subscore, exploit_subscore, mod_impact_subscore diff --git a/cvsslib/cvss3/enums.py b/cvsslib/cvss3/enums.py new file mode 100644 index 0000000..eaf06d9 --- /dev/null +++ b/cvsslib/cvss3/enums.py @@ -0,0 +1,163 @@ +from .. import BaseEnum + + +# Taken from https://www.first.org/cvss/specification-document#i8.4 + +# Exploitability metrics +class AttackVector(BaseEnum): + """ + Vector: AV + Mandatory: yes + """ + NETWORK = 0.85 + ADJACENT_NETWORK = 0.62 + LOCAL = 0.55 + PHYSICAL = 0.2 + + +class AttackComplexity(BaseEnum): + """ + Vector: AC + Mandatory: yes + """ + LOW = 0.77 + HIGH = 0.44 + + +class PrivilegeRequired(BaseEnum): + """ + Vector: PR + Mandatory: yes + """ + NONE = 0.85 + LOW = 0.62 + HIGH = 0.27 + + +class UserInteraction(BaseEnum): + """ + Vector: UI + Mandatory: yes + """ + NONE = 0.85 + REQUIRED = 0.62 + + +class Scope(BaseEnum): + """ + Vector: S + Mandatory: yes + """ + UNCHANGED = 0 + CHANGED = 1 + + +# Impacts +class ConfidentialityImpact(BaseEnum): + """ + Vector: C + Mandatory: yes + """ + HIGH = 0.56 + LOW = 0.22 + NONE = 0 + + +class IntegrityImpact(BaseEnum): + """ + Vector: I + Mandatory: yes + """ + HIGH = 0.56 + LOW = 0.22 + NONE = 0 + + +class AvailabilityImpact(BaseEnum): + """ + Vector: A + Mandatory: yes + """ + HIGH = 0.56 + LOW = 0.22 + NONE = 0 + + +# Temporal metrics +class ExploitCodeMaturity(BaseEnum): + """ + Vector: E + """ + NOT_DEFINED = 1 + HIGH = 1 + FUNCTIONAL = 0.97 + PROOF_OF_CONCEPT = 0.94 + UNPROVEN = 0.91 + + +class RemediationLevel(BaseEnum): + """ + Vector: RL + """ + NOT_DEFINED = 1 + UNAVAILABLE = 1 + WORKAROUND = 0.97 + TEMPORARY_FIX = 0.96 + OFFICIAL_FIX = 0.95 + + +class ReportConfidence(BaseEnum): + """ + Vector: RC + """ + NOT_DEFINED = 1 + CONFIRMED = 1 + REASONABLE = 0.96 + UNKNOWN = 0.92 + + +class ConfidentialityRequirements(BaseEnum): + """ + Vector: CR + """ + NOT_DEFINED = 1 + HIGH = 1.5 + MEDIUM = 1 + LOW = 0.5 + + +class IntegrityRequirements(BaseEnum): + """ + Vector: IR + """ + NOT_DEFINED = 1 + HIGH = 1.5 + MEDIUM = 1 + LOW = 0.5 + + +class AvailabilityRequirements(BaseEnum): + """ + Vector: AR + """ + NOT_DEFINED = 1 + HIGH = 1.5 + MEDIUM = 1 + LOW = 0.5 + + +ModifiedAttackVector = AttackVector.extend("ModifiedAttackVector", {"NOT_DEFINED": 0.85}, "Vector: MAV") + +ModifiedAttackComplexity = AttackComplexity.extend("ModifiedAttackComplexity", {"NOT_DEFINED": 0.77}, "Vector: MAC") + +ModifiedPrivilegeRequired = PrivilegeRequired.extend("ModifiedPrivilegesRequired", {"NOT_DEFINED": 0.85}, "Vector: MPR") + +ModifiedUserInteraction = UserInteraction.extend("ModifiedUserInteraction", {"NOT_DEFINED": 0.85}, "Vector: MUI") + +ModifiedScope = Scope.extend("ModifiedScope", {"NOT_DEFINED": Scope.UNCHANGED.value}, "Vector: MS") + +ModifiedConfidentiality = ConfidentialityImpact.extend("ModifiedConfidentiality", {"NOT_DEFINED": 1.0}, "Vector: MC") + +ModifiedIntegrity = IntegrityImpact.extend("ModifiedIntegrity", {"NOT_DEFINED": 1.0}, "Vector: MI") + +ModifiedAvailability = AvailabilityImpact.extend("ModifiedAvailability", {"NOT_DEFINED": 1.0}, "Vector: MA") diff --git a/cvsslib/mixin.py b/cvsslib/mixin.py new file mode 100644 index 0000000..ac5e75c --- /dev/null +++ b/cvsslib/mixin.py @@ -0,0 +1,75 @@ +import functools +import inspect +from utils import get_enums, function_caller + + +def make_attribute_name(str): + """ + Turns strings like AttackVector and ExploitCodeMaturity into + attack_vector and exploit_code_maturity + """ + returner = "" + + for idx, char in enumerate(str): + if char.isupper() and idx != 0: + returner += "_" + returner += char.lower() + + return returner + + +def django_mixin(module, base=None): + # This is a function that takes a module (filled with enums and a function called 'calculate') + # and wires it up into a Django model that we can use. + + from django.db import models + from django.db.models.base import ModelBase + + base = base or ModelBase + + class ScoringMetaclass(base): + @classmethod + def __prepare__(mcs, *args, **kwargs): + returner = super().__prepare__(*args, **kwargs) + + enum_dict = {} + + for name, obj in get_enums(module): + attr_name = make_attribute_name(name) + choices = obj.choices() + + if hasattr(obj, "NONE"): + default = obj.NONE.value + elif hasattr(obj, "NOT_DEFINED"): + default = obj.NOT_DEFINED.value + else: + default = min(o.value for o in obj) + + nullable = any(o.value is None for o in obj) + + returner[attr_name] = models.DecimalField(max_digits=7, + decimal_places=4, + choices=choices, + default=default, + null=nullable) + + enum_dict[obj] = attr_name + + calculate_func = getattr(module, "calculate", None) + + if not calculate_func: + raise RuntimeError("Cannot find 'calculate' method in {module}".format(module=module)) + + # Make the 'calculate' method + def model_calculate(self): + def _getter(enum_type): + member_name = enum_dict[enum_type] + return getattr(self, member_name) + + return calculate_func(function_caller(_getter)) + + returner["calculate"] = model_calculate + + return returner + + return ScoringMetaclass diff --git a/cvsslib/vector.py b/cvsslib/vector.py new file mode 100644 index 0000000..15b783e --- /dev/null +++ b/cvsslib/vector.py @@ -0,0 +1,60 @@ +import inspect + +from cvsslib import cvss3 +from cvsslib.utils import get_enums, function_caller + +v3_vector_map = {} +v3_mandatory = set() + +for name, enum in get_enums(cvss3): + docstring = inspect.getdoc(enum) + lines = docstring.strip().split("\n") + options = { + line.split(":")[0].lower().strip(): line.split(":")[1].strip() + for line in lines + } + + vector_name = options["vector"] + v3_vector_map[vector_name] = enum + if options.get("mandatory", "") == "yes": + v3_mandatory.add(vector_name) + + +def parse_vector(vector): + if vector.startswith("CVSS:3.0"): + return parse_cvss3_vector(vector) + return parse_cvss2_vector(vector) + + +def parse_cvss3_vector(vec): + split = vec.split("/") + + vector_values = {} + given_keys = set() + + for part in split: + if not part: + continue + + key, value = part.split(":") + if key == "CVSS": + continue + + if key not in v3_vector_map: + raise RuntimeError("Unknown part {0} in vector".format(part)) + + enum = v3_vector_map[key] + value_from_key = enum.get_value_from_vector(value) + vector_values[enum] = value_from_key + given_keys.add(key) + print("{key}: {enum}: {value}".format(key=key, enum=enum, value=value)) + + required_diff = v3_mandatory.difference(given_keys) + + if required_diff: + raise RuntimeError("Missing mandatory keys {0}".format(required_diff)) + + def _getter(enum_type): + return vector_values[enum_type] + + return cvss3.calculate(function_caller(_getter)) diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..0655ce7 --- /dev/null +++ b/setup.py @@ -0,0 +1,12 @@ +from distutils.core import setup + +setup( + name='cvsslib', + version='0.1', + packages=['cvsslib', 'cvsslib.cvss2', 'cvsslib.cvss3'], + url='', + license='', + author='Tom', + author_email='', + description='CVSS 2/3 utilities' +)