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

1# -*- coding: utf-8 -*- 

2# SPDX-License-Identifier: GPL-2.0-only 

3 

4import abc 

5import dataclasses 

6from datetime import datetime 

7from enum import Enum, auto 

8 

9from .cvss import CvssMetric 

10 

11 

12class CveId: 

13 def __init__(self, cve_id: str) -> None: 

14 self._id = cve_id 

15 

16 @property 

17 def id(self) -> str: 

18 return self._id 

19 

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] 

26 

27 @property 

28 def year(self) -> str: 

29 return self.year_and_number[0] 

30 

31 @property 

32 def number(self) -> str: 

33 return self.year_and_number[1] 

34 

35 def __hash__(self) -> int: 

36 return hash(self._id) 

37 

38 def __eq__(self, other: object) -> bool: 

39 if not isinstance(other, CveId): 

40 return False 

41 return other._id == self._id 

42 

43 def __repr__(self) -> str: 

44 return self._id 

45 

46 

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) 

52 

53 

54class CveVexStatus(Enum): 

55 UNKNOWN = None 

56 UNDER_INVESTIGATION = "under_investigation" 

57 AFFECTED = "affected" 

58 NOT_AFFECTED = "not_affected" 

59 FIXED = "fixed" 

60 

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 

67 

68 

69@dataclasses.dataclass(kw_only=True) 

70class CveVexAssessment(abc.ABC): 

71 vex_version: str | None = None 

72 status_notes: str | None = None 

73 

74 @property 

75 @abc.abstractmethod 

76 def status(self) -> CveVexStatus: 

77 pass 

78 

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 

88 

89 @property 

90 def detail(self) -> str | None: 

91 """Provide statement and status notes""" 

92 return self._detail([]) 

93 

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) 

99 

100 

101@dataclasses.dataclass(kw_only=True) 

102class CveVexFixedAssessment(CveVexAssessment): 

103 @property 

104 def status(self) -> CveVexStatus: 

105 return CveVexStatus.FIXED 

106 

107 

108@dataclasses.dataclass(kw_only=True) 

109class CveVexOldVersNotVulnAssessment(CveVexFixedAssessment): 

110 pass 

111 

112 

113@dataclasses.dataclass(kw_only=True) 

114class CveVexAffectedAssessment(CveVexAssessment): 

115 action_statement: str 

116 action_statement_time: datetime | None = None 

117 

118 @property 

119 def status(self) -> CveVexStatus: 

120 return CveVexStatus.AFFECTED 

121 

122 @property 

123 def detail(self) -> str | None: 

124 return self._detail([self.action_statement]) 

125 

126 

127@dataclasses.dataclass(kw_only=True) 

128class CveVexMissingVersionsAssessment(CveVexAffectedAssessment): 

129 pass 

130 

131 

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() 

138 

139 

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 

145 

146 @property 

147 def status(self) -> CveVexStatus: 

148 return CveVexStatus.NOT_AFFECTED 

149 

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 ) 

158 

159 

160@dataclasses.dataclass(kw_only=True) 

161class CveVexRejectedAssessment(CveVexNotAffectedAssessment): 

162 pass 

163 

164 

165@dataclasses.dataclass(kw_only=True) 

166class CveVexUnderInvestigationAssessment(CveVexAssessment): 

167 @property 

168 def status(self) -> CveVexStatus: 

169 return CveVexStatus.UNDER_INVESTIGATION 

170 

171 

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 

177 

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 } 

185 

186 return a if assessment_order[a.status] >= assessment_order[b.status] else b 

187 

188 

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