src/skjold/cvss.py

Summary

Maintainability
A
30 mins
Test Coverage
A
100%
import math
from typing import Any, Dict, Union


def round_up(n: float, decimals: int = 1) -> float:
    multiplier = 10**decimals
    return float(math.ceil(n * multiplier) / multiplier)


class CVSS2:

    _basic: Dict[str, str] = {}
    _fields_basic_group = frozenset({"AV", "AC", "Au", "C", "I", "A"})
    _metrics: Dict[str, Any] = {
        "AV": {"N": 1.0, "A": 0.646, "L": 0.395},
        "AC": {"L": 0.71, "M": 0.61, "H": 0.35},
        "Au": {"M": 0.45, "S": 0.56, "N": 0.704},
        "C": {"N": 0.0, "P": 0.275, "C": 0.660},
        "I": {"N": 0.0, "P": 0.275, "C": 0.660},
        "A": {"N": 0.0, "P": 0.275, "C": 0.660},
    }

    @classmethod
    def using(cls, vector: str) -> "CVSS2":
        kv = map(lambda v: v.split(":"), vector.strip().split("/"))
        basic_group = filter(lambda metric: metric[0] in CVSS2._fields_basic_group, kv)

        obj = CVSS2()
        obj._basic = {v[0]: v[1] for v in basic_group}
        return obj

    @property
    def _impact_subscore(self) -> float:
        confidentiality = self._basic["C"]
        integrity = self._basic["I"]
        availability = self._basic["A"]
        return 1 - (
            (1 - float(self._metrics["C"][confidentiality]))
            * (1 - float(self._metrics["I"][integrity]))
            * (1 - float(self._metrics["A"][availability]))
        )

    @property
    def exploitability_score(self) -> float:
        """Calculate the exploitability score."""
        av = float(self._metrics["AV"][self._basic["AV"]])
        ac = float(self._metrics["AC"][self._basic["AC"]])
        au = float(self._metrics["Au"][self._basic["Au"]])
        return 20.0 * av * ac * au

    @property
    def impact_score(self) -> float:
        """Calculate the impact score."""
        return 10.41 * self._impact_subscore

    @property
    def score(self) -> float:
        """Return the CVSS 2.0 base score."""
        if self.impact_score <= 0:
            return 0.0

        return round(
            1.176
            * ((0.6 * self.impact_score) + (0.4 * self.exploitability_score) - 1.5),
            1,
        )

    @property
    def severity(self) -> str:
        """Return severity level for based on the CVSS 2.0 base score."""
        x = self.score
        if x < 0.001:
            return "NONE"
        elif x < 4:
            return "LOW"
        elif x < 7:
            return "MEDIUM"
        else:
            return "HIGH"


class CVSS3:

    _basic: Dict[str, str] = {}
    _fields_basic_group = frozenset({"AV", "AC", "PR", "UI", "S", "C", "I", "A"})
    _metrics: Dict[str, Any] = {
        "AV": {"N": 0.85, "A": 0.62, "L": 0.55, "P": 0.2},
        "AC": {"L": 0.77, "H": 0.44},
        "PR": {
            "C": {"N": 0.85, "L": 0.68, "H": 0.50},
            "U": {"N": 0.85, "L": 0.62, "H": 0.27},
        },
        "UI": {"N": 0.85, "R": 0.62},
        "C": {"H": 0.56, "L": 0.22, "N": 0.0},
        "I": {"H": 0.56, "L": 0.22, "N": 0.0},
        "A": {"H": 0.56, "L": 0.22, "N": 0.0},
    }

    @classmethod
    def using(cls, vector: str) -> "CVSS3":
        kv = map(lambda v: v.split(":"), vector.strip().upper().split("/"))
        basic_group = filter(lambda metric: metric[0] in cls._fields_basic_group, kv)

        obj = CVSS3()
        obj._basic = {v[0]: v[1] for v in basic_group}
        return obj

    @property
    def scope(self) -> str:
        return self._basic["S"]

    @property
    def _impact_subscore(self) -> float:
        confidentiality = self._basic["C"]
        integrity = self._basic["I"]
        availability = self._basic["A"]
        return 1 - (
            (1 - float(self._metrics["C"][confidentiality]))
            * (1 - float(self._metrics["I"][integrity]))
            * (1 - float(self._metrics["A"][availability]))
        )

    @property
    def exploitability_score(self) -> float:
        """Calculate the exploitability score."""
        av = float(self._metrics["AV"][self._basic["AV"]])
        ac = float(self._metrics["AC"][self._basic["AC"]])
        pr = float(self._metrics["PR"][self._basic["S"]][self._basic["PR"]])
        ui = float(self._metrics["UI"][self._basic["UI"]])

        return 8.22 * av * ac * pr * ui

    @property
    def impact_score(self) -> float:
        """Calculate the impact score."""
        impact_subscore = self._impact_subscore
        if self.scope == "U":
            return 6.42 * impact_subscore

        return 7.52 * (impact_subscore - 0.029) - 3.25 * (impact_subscore - 0.02) ** 15

    @property
    def score(self) -> float:
        """Return the CVSS 3.0 base score."""
        if self.impact_score <= 0:
            return 0.0

        if self.scope == "U":
            return round_up(min((self.impact_score + self.exploitability_score), 10.0))

        return round_up(
            min(1.08 * (self.impact_score + self.exploitability_score), 10.0)
        )

    @property
    def severity(self) -> str:
        """Return severity level for based on the CVSS 3.0 base score."""
        x = self.score
        if x < 0.001:
            return "NONE"
        elif x < 4.0:
            return "LOW"
        elif x < 7.0:
            return "MEDIUM"
        elif x < 9.0:
            return "HIGH"
        else:
            return "CRITICAL"


def parse_cvss(vector: str) -> Union[CVSS2, CVSS3]:
    if vector.startswith("CVSS:3"):
        return CVSS3.using(vector)
    return CVSS2.using(vector)