|
| 1 | +""" |
| 2 | +CVSS v3.1 Score Calculator |
| 3 | +
|
| 4 | +Implements the Common Vulnerability Scoring System v3.1 |
| 5 | +https://www.first.org/cvss/v3.1/specification-document |
| 6 | +""" |
| 7 | + |
| 8 | +from dataclasses import dataclass |
| 9 | +from typing import Dict |
| 10 | +from enum import Enum |
| 11 | + |
| 12 | + |
| 13 | +class AttackVector(Enum): |
| 14 | + """Attack Vector (AV)""" |
| 15 | + NETWORK = ("N", 0.85) |
| 16 | + ADJACENT = ("A", 0.62) |
| 17 | + LOCAL = ("L", 0.55) |
| 18 | + PHYSICAL = ("P", 0.2) |
| 19 | + |
| 20 | + |
| 21 | +class AttackComplexity(Enum): |
| 22 | + """Attack Complexity (AC)""" |
| 23 | + LOW = ("L", 0.77) |
| 24 | + HIGH = ("H", 0.44) |
| 25 | + |
| 26 | + |
| 27 | +class PrivilegesRequired(Enum): |
| 28 | + """Privileges Required (PR)""" |
| 29 | + NONE = ("N", 0.85, 0.85) # (abbr, unchanged, changed) |
| 30 | + LOW = ("L", 0.62, 0.68) |
| 31 | + HIGH = ("H", 0.27, 0.50) |
| 32 | + |
| 33 | + |
| 34 | +class UserInteraction(Enum): |
| 35 | + """User Interaction (UI)""" |
| 36 | + NONE = ("N", 0.85) |
| 37 | + REQUIRED = ("R", 0.62) |
| 38 | + |
| 39 | + |
| 40 | +class Scope(Enum): |
| 41 | + """Scope (S)""" |
| 42 | + UNCHANGED = ("U", False) |
| 43 | + CHANGED = ("C", True) |
| 44 | + |
| 45 | + |
| 46 | +class Impact(Enum): |
| 47 | + """Impact metrics (C/I/A)""" |
| 48 | + NONE = ("N", 0.0) |
| 49 | + LOW = ("L", 0.22) |
| 50 | + HIGH = ("H", 0.56) |
| 51 | + |
| 52 | + |
| 53 | +@dataclass |
| 54 | +class CVSSVector: |
| 55 | + """CVSS v3.1 Vector""" |
| 56 | + attack_vector: AttackVector |
| 57 | + attack_complexity: AttackComplexity |
| 58 | + privileges_required: PrivilegesRequired |
| 59 | + user_interaction: UserInteraction |
| 60 | + scope: Scope |
| 61 | + confidentiality: Impact |
| 62 | + integrity: Impact |
| 63 | + availability: Impact |
| 64 | + |
| 65 | + def to_string(self) -> str: |
| 66 | + """Convert to CVSS vector string""" |
| 67 | + return ( |
| 68 | + f"CVSS:3.1/" |
| 69 | + f"AV:{self.attack_vector.value[0]}/" |
| 70 | + f"AC:{self.attack_complexity.value[0]}/" |
| 71 | + f"PR:{self.privileges_required.value[0]}/" |
| 72 | + f"UI:{self.user_interaction.value[0]}/" |
| 73 | + f"S:{self.scope.value[0]}/" |
| 74 | + f"C:{self.confidentiality.value[0]}/" |
| 75 | + f"I:{self.integrity.value[0]}/" |
| 76 | + f"A:{self.availability.value[0]}" |
| 77 | + ) |
| 78 | + |
| 79 | + |
| 80 | +class CVSSCalculator: |
| 81 | + """ |
| 82 | + CVSS v3.1 Score Calculator |
| 83 | + |
| 84 | + Calculates Base, Temporal, and Environmental scores |
| 85 | + according to CVSS v3.1 specification. |
| 86 | + """ |
| 87 | + |
| 88 | + @staticmethod |
| 89 | + def calculate_base_score(vector: CVSSVector) -> float: |
| 90 | + """ |
| 91 | + Calculate CVSS v3.1 Base Score |
| 92 | + |
| 93 | + Args: |
| 94 | + vector: CVSS vector with all metrics |
| 95 | + |
| 96 | + Returns: |
| 97 | + Base score (0.0 - 10.0) |
| 98 | + """ |
| 99 | + # Get metric values |
| 100 | + av = vector.attack_vector.value[1] |
| 101 | + ac = vector.attack_complexity.value[1] |
| 102 | + |
| 103 | + # PR depends on scope |
| 104 | + if vector.scope == Scope.UNCHANGED: |
| 105 | + pr = vector.privileges_required.value[1] |
| 106 | + else: |
| 107 | + pr = vector.privileges_required.value[2] |
| 108 | + |
| 109 | + ui = vector.user_interaction.value[1] |
| 110 | + c = vector.confidentiality.value[1] |
| 111 | + i = vector.integrity.value[1] |
| 112 | + a = vector.availability.value[1] |
| 113 | + |
| 114 | + # Calculate Impact Sub Score (ISS) |
| 115 | + iss = 1 - ((1 - c) * (1 - i) * (1 - a)) |
| 116 | + |
| 117 | + # Calculate Impact |
| 118 | + if vector.scope == Scope.UNCHANGED: |
| 119 | + impact = 6.42 * iss |
| 120 | + else: |
| 121 | + impact = 7.52 * (iss - 0.029) - 3.25 * pow(iss - 0.02, 15) |
| 122 | + |
| 123 | + # Calculate Exploitability |
| 124 | + exploitability = 8.22 * av * ac * pr * ui |
| 125 | + |
| 126 | + # Calculate Base Score |
| 127 | + if impact <= 0: |
| 128 | + return 0.0 |
| 129 | + |
| 130 | + if vector.scope == Scope.UNCHANGED: |
| 131 | + base_score = min(impact + exploitability, 10.0) |
| 132 | + else: |
| 133 | + base_score = min(1.08 * (impact + exploitability), 10.0) |
| 134 | + |
| 135 | + # Round up to 1 decimal |
| 136 | + return round(base_score * 10) / 10 |
| 137 | + |
| 138 | + @staticmethod |
| 139 | + def get_severity(score: float) -> str: |
| 140 | + """Get severity rating from score""" |
| 141 | + if score == 0.0: |
| 142 | + return "NONE" |
| 143 | + elif score < 4.0: |
| 144 | + return "LOW" |
| 145 | + elif score < 7.0: |
| 146 | + return "MEDIUM" |
| 147 | + elif score < 9.0: |
| 148 | + return "HIGH" |
| 149 | + else: |
| 150 | + return "CRITICAL" |
| 151 | + |
| 152 | + @staticmethod |
| 153 | + def parse_vector_string(vector_string: str) -> CVSSVector: |
| 154 | + """ |
| 155 | + Parse CVSS vector string |
| 156 | + |
| 157 | + Args: |
| 158 | + vector_string: e.g., "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H" |
| 159 | + |
| 160 | + Returns: |
| 161 | + CVSSVector object |
| 162 | + """ |
| 163 | + # Remove CVSS:3.1/ prefix |
| 164 | + if vector_string.startswith("CVSS:3.1/"): |
| 165 | + vector_string = vector_string[9:] |
| 166 | + |
| 167 | + metrics = {} |
| 168 | + for part in vector_string.split("/"): |
| 169 | + key, value = part.split(":") |
| 170 | + metrics[key] = value |
| 171 | + |
| 172 | + # Parse each metric |
| 173 | + av_map = {v.value[0]: v for v in AttackVector} |
| 174 | + ac_map = {v.value[0]: v for v in AttackComplexity} |
| 175 | + pr_map = {v.value[0]: v for v in PrivilegesRequired} |
| 176 | + ui_map = {v.value[0]: v for v in UserInteraction} |
| 177 | + s_map = {v.value[0]: v for v in Scope} |
| 178 | + i_map = {v.value[0]: v for v in Impact} |
| 179 | + |
| 180 | + return CVSSVector( |
| 181 | + attack_vector=av_map[metrics["AV"]], |
| 182 | + attack_complexity=ac_map[metrics["AC"]], |
| 183 | + privileges_required=pr_map[metrics["PR"]], |
| 184 | + user_interaction=ui_map[metrics["UI"]], |
| 185 | + scope=s_map[metrics["S"]], |
| 186 | + confidentiality=i_map[metrics["C"]], |
| 187 | + integrity=i_map[metrics["I"]], |
| 188 | + availability=i_map[metrics["A"]] |
| 189 | + ) |
| 190 | + |
| 191 | + @classmethod |
| 192 | + def calculate_from_string(cls, vector_string: str) -> Dict: |
| 193 | + """ |
| 194 | + Calculate score from vector string |
| 195 | + |
| 196 | + Returns: |
| 197 | + Dict with score, severity, and vector |
| 198 | + """ |
| 199 | + vector = cls.parse_vector_string(vector_string) |
| 200 | + score = cls.calculate_base_score(vector) |
| 201 | + severity = cls.get_severity(score) |
| 202 | + |
| 203 | + return { |
| 204 | + "score": score, |
| 205 | + "severity": severity, |
| 206 | + "vector_string": vector.to_string() |
| 207 | + } |
| 208 | + |
| 209 | + |
| 210 | +# Common vulnerability patterns |
| 211 | +CVSS_TEMPLATES = { |
| 212 | + "sql_injection": CVSSVector( |
| 213 | + attack_vector=AttackVector.NETWORK, |
| 214 | + attack_complexity=AttackComplexity.LOW, |
| 215 | + privileges_required=PrivilegesRequired.NONE, |
| 216 | + user_interaction=UserInteraction.NONE, |
| 217 | + scope=Scope.CHANGED, |
| 218 | + confidentiality=Impact.HIGH, |
| 219 | + integrity=Impact.HIGH, |
| 220 | + availability=Impact.HIGH |
| 221 | + ), # 9.8 CRITICAL |
| 222 | + |
| 223 | + "xss_reflected": CVSSVector( |
| 224 | + attack_vector=AttackVector.NETWORK, |
| 225 | + attack_complexity=AttackComplexity.LOW, |
| 226 | + privileges_required=PrivilegesRequired.NONE, |
| 227 | + user_interaction=UserInteraction.REQUIRED, |
| 228 | + scope=Scope.CHANGED, |
| 229 | + confidentiality=Impact.LOW, |
| 230 | + integrity=Impact.LOW, |
| 231 | + availability=Impact.NONE |
| 232 | + ), # 6.1 MEDIUM |
| 233 | + |
| 234 | + "auth_bypass": CVSSVector( |
| 235 | + attack_vector=AttackVector.NETWORK, |
| 236 | + attack_complexity=AttackComplexity.LOW, |
| 237 | + privileges_required=PrivilegesRequired.NONE, |
| 238 | + user_interaction=UserInteraction.NONE, |
| 239 | + scope=Scope.UNCHANGED, |
| 240 | + confidentiality=Impact.HIGH, |
| 241 | + integrity=Impact.HIGH, |
| 242 | + availability=Impact.HIGH |
| 243 | + ), # 9.8 CRITICAL |
| 244 | +} |
0 commit comments