Coverage for /home/benjarobin/Bootlin/projects/Schneider-Electric-Senux/sbom-cve-check/src/sbom_cve_check/vuln/cve.py: 83%
132 statements
« prev ^ index » next coverage.py v7.11.1, created at 2025-11-28 15:37 +0100
« prev ^ index » next coverage.py v7.11.1, created at 2025-11-28 15:37 +0100
1# -*- coding: utf-8 -*-
2# SPDX-License-Identifier: GPL-2.0-only
4import abc
5import dataclasses
6from datetime import datetime
7from enum import Enum, auto
9from .cvss import CvssMetric
12class CveId:
13 def __init__(self, cve_id: str) -> None:
14 self._id = cve_id
16 @property
17 def id(self) -> str:
18 return self._id
20 @property
21 def year_and_number(self) -> tuple[str, str]:
22 parts = self._id.split("-")
23 if (len(parts) != 3) or (parts[0] != "CVE"):
24 raise ValueError(f"Invalid CVE identifier: {self._id}")
25 return parts[1], parts[2]
27 @property
28 def year(self) -> str:
29 return self.year_and_number[0]
31 @property
32 def number(self) -> str:
33 return self.year_and_number[1]
35 def __hash__(self) -> int:
36 return hash(self._id)
38 def __eq__(self, other: object) -> bool:
39 if not isinstance(other, CveId):
40 return False
41 return other._id == self._id
43 def __repr__(self) -> str:
44 return self._id
47@dataclasses.dataclass(frozen=True, order=True)
48class CveExtReference:
49 url: str
50 # ExternalRefType
51 ref_type: str | None = dataclasses.field(default=None, hash=False, compare=False)
54class CveVexStatus(Enum):
55 UNKNOWN = None
56 UNDER_INVESTIGATION = "under_investigation"
57 AFFECTED = "affected"
58 NOT_AFFECTED = "not_affected"
59 FIXED = "fixed"
61 def is_vulnerable(self) -> bool | None:
62 if self in (CveVexStatus.NOT_AFFECTED, CveVexStatus.FIXED):
63 return False
64 if self in (CveVexStatus.AFFECTED, CveVexStatus.UNDER_INVESTIGATION):
65 return True
66 return None
69@dataclasses.dataclass(kw_only=True)
70class CveVexAssessment(abc.ABC):
71 vex_version: str | None = None
72 status_notes: str | None = None
74 @property
75 @abc.abstractmethod
76 def status(self) -> CveVexStatus:
77 pass
79 def _detail(self, extra: list[str | None]) -> str | None:
80 lst = []
81 if self.status_notes:
82 lst.append(self.status_notes)
83 if extra:
84 lst.extend(d for d in extra if d)
85 if lst:
86 return ": ".join(lst)
87 return None
89 @property
90 def detail(self) -> str | None:
91 """Provide statement and status notes"""
92 return self._detail([])
94 def __str__(self) -> str:
95 d = self.detail
96 if d:
97 return f"{self.status.value} ({d})"
98 return str(self.status.value)
101@dataclasses.dataclass(kw_only=True)
102class CveVexFixedAssessment(CveVexAssessment):
103 @property
104 def status(self) -> CveVexStatus:
105 return CveVexStatus.FIXED
108@dataclasses.dataclass(kw_only=True)
109class CveVexOldVersNotVulnAssessment(CveVexFixedAssessment):
110 pass
113@dataclasses.dataclass(kw_only=True)
114class CveVexAffectedAssessment(CveVexAssessment):
115 action_statement: str
116 action_statement_time: datetime | None = None
118 @property
119 def status(self) -> CveVexStatus:
120 return CveVexStatus.AFFECTED
122 @property
123 def detail(self) -> str | None:
124 return self._detail([self.action_statement])
127@dataclasses.dataclass(kw_only=True)
128class CveVexMissingVersionsAssessment(CveVexAffectedAssessment):
129 pass
132class CveVexJustification(Enum):
133 COMPONENT_NOT_PRESENT = auto()
134 INLINE_MITIGATIONS_ALREADY_EXIST = auto()
135 VULNERABLE_CODE_CANNOT_BE_CONTROLLED_BY_ADVERSARY = auto()
136 VULNERABLE_CODE_NOT_IN_EXECUTE_PATH = auto()
137 VULNERABLE_CODE_NOT_PRESENT = auto()
140@dataclasses.dataclass(kw_only=True)
141class CveVexNotAffectedAssessment(CveVexAssessment):
142 impact_statement: str
143 impact_statement_time: datetime | None = None
144 justification: CveVexJustification | None = None
146 @property
147 def status(self) -> CveVexStatus:
148 return CveVexStatus.NOT_AFFECTED
150 @property
151 def detail(self) -> str | None:
152 return self._detail(
153 [
154 self.justification.name.lower() if self.justification else None,
155 self.impact_statement,
156 ]
157 )
160@dataclasses.dataclass(kw_only=True)
161class CveVexRejectedAssessment(CveVexNotAffectedAssessment):
162 pass
165@dataclasses.dataclass(kw_only=True)
166class CveVexUnderInvestigationAssessment(CveVexAssessment):
167 @property
168 def status(self) -> CveVexStatus:
169 return CveVexStatus.UNDER_INVESTIGATION
172def find_most_appropriate_assessment(
173 a: CveVexAssessment | None, b: CveVexAssessment | None
174) -> CveVexAssessment | None:
175 if (a is None) or (b is None):
176 return a or b
178 assessment_order = {
179 CveVexStatus.UNKNOWN: 0,
180 CveVexStatus.NOT_AFFECTED: 1,
181 CveVexStatus.FIXED: 2,
182 CveVexStatus.UNDER_INVESTIGATION: 3,
183 CveVexStatus.AFFECTED: 4,
184 }
186 return a if assessment_order[a.status] >= assessment_order[b.status] else b
189@dataclasses.dataclass
190class CveInfo:
191 cve_id: CveId
192 date_published: datetime | None = None
193 date_modified: datetime | None = None
194 description: str | None = None
195 cvss_metrics: list[CvssMetric] = dataclasses.field(default_factory=list)
196 references: list[CveExtReference] = dataclasses.field(default_factory=list)
197 vex_assessment: CveVexAssessment | None = None