Coverage for csaf/product.py: 81.91%

126 statements  

« prev     ^ index     » next       coverage.py v7.4.1, created at 2024-02-04 16:28:45 +00:00

1"""CSAF Product Tree model.""" 

2 

3from __future__ import annotations 

4 

5import re 

6from collections.abc import Sequence 

7from enum import Enum 

8from typing import Annotated, List, Optional, no_type_check 

9 

10from pydantic import BaseModel, Field, RootModel, field_validator, model_validator 

11 

12from csaf.definitions import AnyUrl, Products, ReferenceTokenForProductGroupInstance, ReferenceTokenForProductInstance 

13 

14 

15class FileHash(BaseModel): 

16 """ 

17 Contains one hash value and algorithm of the file to be identified. 

18 """ 

19 

20 algorithm: Annotated[ 

21 str, 

22 Field( 

23 description='Contains the name of the cryptographic hash algorithm used to calculate the value.', 

24 examples=['blake2b512', 'sha256', 'sha3-512', 'sha384', 'sha512'], 

25 min_length=1, 

26 title='Algorithm of the cryptographic hash', 

27 ), 

28 ] 

29 value: Annotated[ 

30 str, 

31 Field( 

32 description='Contains the cryptographic hash value in hexadecimal representation.', 

33 examples=[ 

34 ( 

35 '37df33cb7464da5c7f077f4d56a32bc84987ec1d85b234537c1c1a4d4fc8d09d' 

36 'c29e2e762cb5203677bf849a2855a0283710f1f5fe1d6ce8d5ac85c645d0fcb3' 

37 ), 

38 '4775203615d9534a8bfca96a93dc8b461a489f69124a130d786b42204f3341cc', 

39 '9ea4c8200113d49d26505da0e02e2f49055dc078d1ad7a419b32e291c7afebbb84badfbd46dec42883bea0b2a1fa697c', 

40 ], 

41 min_length=32, 

42 pattern='^[0-9a-fA-F]{32,}$', 

43 title='Value of the cryptographic hash', 

44 ), 

45 ] 

46 

47 

48class CryptographicHashes(BaseModel): 

49 """ 

50 Contains all information to identify a file based on its cryptographic hash values. 

51 """ 

52 

53 file_hashes: Annotated[ 

54 Sequence[FileHash], 

55 Field( 

56 description='Contains a list of cryptographic hashes for this file.', 

57 # min_items=1, 

58 title='List of file hashes', 

59 ), 

60 ] 

61 filename: Annotated[ 

62 str, 

63 Field( 

64 description='Contains the name of the file which is identified by the hash values.', 

65 examples=['WINWORD.EXE', 'msotadddin.dll', 'sudoers.so'], 

66 # min_length=1, 

67 title='Filename', 

68 ), 

69 ] 

70 

71 @classmethod 

72 @no_type_check 

73 @field_validator('file_hashes', 'filename') 

74 def check_len(cls, v): 

75 if not v: 

76 raise ValueError('mandatory element present but empty') 

77 return v 

78 

79 

80class GenericUri(BaseModel): 

81 """ 

82 Provides a generic extension point for any identifier which is either vendor-specific or 

83 derived from a standard not yet supported. 

84 """ 

85 

86 namespace: Annotated[ 

87 AnyUrl, 

88 Field( 

89 description=( 

90 'Refers to a URL which provides the name and knowledge about the specification used or' 

91 ' is the namespace in which these values are valid.' 

92 ), 

93 title='Namespace of the generic URI', 

94 ), 

95 ] 

96 uri: Annotated[AnyUrl, Field(description='Contains the identifier itself.', title='URI')] 

97 

98 

99class SerialNumber( 

100 RootModel[ 

101 Annotated[ 

102 str, 

103 Field( 

104 description='Contains a part, or a full serial number of the component to identify.', 

105 min_length=1, 

106 title='Serial number', 

107 ), 

108 ] 

109 ] 

110): 

111 pass 

112 

113 

114class StockKeepingUnit( 

115 RootModel[ 

116 Annotated[ 

117 str, 

118 Field( 

119 description=( 

120 'Contains a part, or a full stock keeping unit (SKU) which is used in the ordering process' 

121 ' to identify the component.' 

122 ), 

123 min_length=1, 

124 title='Stock keeping unit', 

125 ), 

126 ] 

127 ] 

128): 

129 pass 

130 

131 

132class HelperToIdentifyTheProduct(BaseModel): 

133 """ 

134 Provides at least one method which aids in identifying the product in an asset database. 

135 """ 

136 

137 cpe: Annotated[ 

138 Optional[str], 

139 Field( 

140 description=( 

141 'The Common Platform Enumeration (CPE) attribute refers to a method for naming platforms external' 

142 ' to this specification.' 

143 ), 

144 min_length=5, 

145 pattern=( 

146 '^(cpe:2\\.3:[aho\\*\\-](:(((\\?*|\\*?)([a-zA-Z0-9\\-\\._]|' 

147 '(\\\\[\\\\\\*\\?!"#\\$%&\'\\(\\)\\+,/:;<=>@\\[\\]\\^`\\{\\|\\}~]))+(\\?*|\\*?))|[\\*\\-])){5}' 

148 '(:(([a-zA-Z]{2,3}(-([a-zA-Z]{2}|[0-9]{3}))?)|[\\*\\-]))(:(((\\?*|\\*?)([a-zA-Z0-9\\-\\._]|' 

149 '(\\\\[\\\\\\*\\?!"#\\$%&\'\\(\\)\\+,/:;<=>@\\[\\]\\^`\\{\\|\\}~]))+(\\?*|\\*?))|[\\*\\-])){4})|' 

150 '([c][pP][eE]:/[AHOaho]?(:[A-Za-z0-9\\._\\-~%]*){0,6})$' 

151 ), 

152 title='Common Platform Enumeration representation', 

153 ), 

154 ] = None 

155 hashes: Annotated[ 

156 Optional[Sequence[CryptographicHashes]], 

157 Field( 

158 description='Contains a list of cryptographic hashes usable to identify files.', 

159 # min_items=1, 

160 title='List of hashes', 

161 ), 

162 ] = None 

163 purl: Annotated[ 

164 Optional[AnyUrl], 

165 Field( 

166 description=( 

167 'The package URL (purl) attribute refers to a method for reliably identifying and' 

168 ' locating software packages external to this specification.' 

169 ), 

170 # min_length=7, 

171 # regex='^pkg:[A-Za-z\\.\\-\\+][A-Za-z0-9\\.\\-\\+]*/.+', 

172 title='package URL representation', 

173 ), 

174 ] = None 

175 sbom_urls: Annotated[ 

176 Optional[Sequence[AnyUrl]], 

177 Field( 

178 description='Contains a list of URLs where SBOMs for this product can be retrieved.', 

179 # min_items=1, 

180 title='List of SBOM URLs', 

181 ), 

182 ] = None 

183 serial_numbers: Annotated[ 

184 Optional[Sequence[SerialNumber]], 

185 Field( 

186 description='Contains a list of parts, or full serial numbers.', 

187 # min_items=1, 

188 title='List of serial numbers', 

189 ), 

190 ] = None 

191 skus: Annotated[ 

192 Optional[Sequence[StockKeepingUnit]], 

193 Field( 

194 description='Contains a list of parts, or full stock keeping units.', 

195 # min_items=1, 

196 title='List of stock keeping units', 

197 ), 

198 ] = None 

199 x_generic_uris: Annotated[ 

200 Optional[Sequence[GenericUri]], 

201 Field( 

202 description=( 

203 'Contains a list of identifiers which are either vendor-specific or derived from' 

204 ' a standard not yet supported.' 

205 ), 

206 # min_items=1, 

207 title='List of generic URIs', 

208 ), 

209 ] = None 

210 

211 @classmethod 

212 @no_type_check 

213 @field_validator('hashes', 'sbom_urls', 'serial_numbers', 'skus', 'x_generic_uris') 

214 def check_len(cls, v): 

215 if not v: 

216 raise ValueError('optional element present but empty') 

217 return v 

218 

219 @classmethod 

220 @no_type_check 

221 @field_validator('purl') 

222 def check_purl(cls, v): 

223 if not v or len(v) < 7: 

224 raise ValueError('optional purl element present but too short') 

225 if not re.match('^pkg:[A-Za-z\\.\\-\\+][A-Za-z0-9\\.\\-\\+]*/.+', v): 

226 raise ValueError('optional purl element present but is no purl (regex does not match)') 

227 return v 

228 

229 

230class FullProductName(BaseModel): 

231 """ 

232 Specifies information about the product and assigns the product_id. 

233 """ 

234 

235 name: Annotated[ 

236 str, 

237 Field( 

238 description=( 

239 "The value should be the product's full canonical name, including version number and other attributes," 

240 ' as it would be used in a human-friendly document.' 

241 ), 

242 examples=[ 

243 'Cisco AnyConnect Secure Mobility Client 2.3.185', 

244 'Microsoft Host Integration Server 2006 Service Pack 1', 

245 ], 

246 min_length=1, 

247 title='Textual description of the product', 

248 ), 

249 ] 

250 product_id: ReferenceTokenForProductInstance 

251 product_identification_helper: Annotated[ 

252 Optional[HelperToIdentifyTheProduct], 

253 Field( 

254 description='Provides at least one method which aids in identifying the product in an asset database.', 

255 title='Helper to identify the product', 

256 ), 

257 ] = None 

258 

259 

260class ProductGroup(BaseModel): 

261 """ 

262 Defines a new logical group of products that can then be referred to in other parts of the document to address 

263 a group of products with a single identifier. 

264 """ 

265 

266 group_id: ReferenceTokenForProductGroupInstance 

267 product_ids: Annotated[ 

268 Sequence[ReferenceTokenForProductInstance], 

269 Field( 

270 description='Lists the product_ids of those products which known as one group in the document.', 

271 # min_items=2, 

272 title='List of Product IDs', 

273 ), 

274 ] 

275 summary: Annotated[ 

276 Optional[str], 

277 Field( 

278 description='Gives a short, optional description of the group.', 

279 examples=[ 

280 'Products supporting Modbus.', 

281 'The x64 versions of the operating system.', 

282 ], 

283 min_length=1, 

284 title='Summary of the product group', 

285 ), 

286 ] = None 

287 

288 @classmethod 

289 @no_type_check 

290 @field_validator('product_ids') 

291 def check_len(cls, v): 

292 if len(v) < 2: 

293 raise ValueError('mandatory element present but too few items') 

294 return v 

295 

296 

297class ProductStatus(BaseModel): 

298 """ 

299 Contains different lists of product_ids which provide details on the status of the referenced product related 

300 to the current vulnerability. 

301 """ 

302 

303 first_affected: Annotated[ 

304 Optional[Products], 

305 Field( 

306 description='These are the first versions of the releases known to be affected by the vulnerability.', 

307 title='First affected', 

308 ), 

309 ] = None 

310 first_fixed: Annotated[ 

311 Optional[Products], 

312 Field( 

313 description='These versions contain the first fix for the vulnerability but may not be the recommended' 

314 ' fixed versions.', 

315 title='First fixed', 

316 ), 

317 ] = None 

318 fixed: Annotated[ 

319 Optional[Products], 

320 Field( 

321 description='These versions contain a fix for the vulnerability but may not be the recommended' 

322 ' fixed versions.', 

323 title='Fixed', 

324 ), 

325 ] = None 

326 known_affected: Annotated[ 

327 Optional[Products], 

328 Field( 

329 description='These versions are known to be affected by the vulnerability.', 

330 title='Known affected', 

331 ), 

332 ] = None 

333 known_not_affected: Annotated[ 

334 Optional[Products], 

335 Field( 

336 description='These versions are known not to be affected by the vulnerability.', 

337 title='Known not affected', 

338 ), 

339 ] = None 

340 last_affected: Annotated[ 

341 Optional[Products], 

342 Field( 

343 description='These are the last versions in a release train known to be affected by the vulnerability.' 

344 ' Subsequently released versions would contain a fix for the vulnerability.', 

345 title='Last affected', 

346 ), 

347 ] = None 

348 recommended: Annotated[ 

349 Optional[Products], 

350 Field( 

351 description='These versions have a fix for the vulnerability and are the vendor-recommended versions for' 

352 ' fixing the vulnerability.', 

353 title='Recommended', 

354 ), 

355 ] = None 

356 under_investigation: Annotated[ 

357 Optional[Products], 

358 Field( 

359 description='It is not known yet whether these versions are or are not affected by the vulnerability.' 

360 ' However, it is still under investigation - the result will be provided in a later release' 

361 ' of the document.', 

362 title='Under investigation', 

363 ), 

364 ] = None 

365 

366 

367class RelationshipCategory(Enum): 

368 """ 

369 Defines the category of relationship for the referenced component. 

370 """ 

371 

372 default_component_of = 'default_component_of' 

373 external_component_of = 'external_component_of' 

374 installed_on = 'installed_on' 

375 installed_with = 'installed_with' 

376 optional_component_of = 'optional_component_of' 

377 

378 

379class Relationship(BaseModel): 

380 """ 

381 Establishes a link between two existing full_product_name_t elements, allowing the document producer to define 

382 a combination of two products that form a new full_product_name entry. 

383 """ 

384 

385 category: Annotated[ 

386 RelationshipCategory, 

387 Field( 

388 description='Defines the category of relationship for the referenced component.', 

389 title='Relationship category', 

390 ), 

391 ] 

392 full_product_name: FullProductName 

393 product_reference: Annotated[ 

394 ReferenceTokenForProductInstance, 

395 Field( 

396 description=( 

397 'Holds a Product ID that refers to the Full Product Name element,' 

398 ' which is referenced as the first element of the relationship.' 

399 ), 

400 title='Product reference', 

401 ), 

402 ] 

403 relates_to_product_reference: Annotated[ 

404 ReferenceTokenForProductInstance, 

405 Field( 

406 description=( 

407 'Holds a Product ID that refers to the Full Product Name element,' 

408 ' which is referenced as the second element of the relationship.' 

409 ), 

410 title='Relates to product reference', 

411 ), 

412 ] 

413 

414 

415class ProductTree(BaseModel): 

416 """ 

417 Is a container for all fully qualified product names that can be referenced elsewhere in the document. 

418 """ 

419 

420 branches: Optional[Branches] = None 

421 full_product_names: Annotated[ 

422 Optional[List[FullProductName]], 

423 Field( 

424 description='Contains a list of full product names.', 

425 min_length=1, 

426 title='List of full product names', 

427 ), 

428 ] = None 

429 product_groups: Annotated[ 

430 Optional[List[ProductGroup]], 

431 Field( 

432 description='Contains a list of product groups.', 

433 min_length=1, 

434 title='List of product groups', 

435 ), 

436 ] = None 

437 relationships: Annotated[ 

438 Optional[List[Relationship]], 

439 Field( 

440 description='Contains a list of relationships.', 

441 min_length=1, 

442 title='List of relationships', 

443 ), 

444 ] = None 

445 

446 @classmethod 

447 @no_type_check 

448 @field_validator('full_product_names', 'product_groups', 'relationships') 

449 def check_len(cls, v): 

450 if not v: 

451 raise ValueError('optional element present but empty') 

452 return v 

453 

454 

455class BranchCategory(Enum): 

456 """ 

457 Describes the characteristics of the labeled branch. 

458 """ 

459 

460 architecture = 'architecture' 

461 host_name = 'host_name' 

462 language = 'language' 

463 legacy = 'legacy' 

464 patch_level = 'patch_level' 

465 product_family = 'product_family' 

466 product_name = 'product_name' 

467 product_version = 'product_version' 

468 service_pack = 'service_pack' 

469 specification = 'specification' 

470 vendor = 'vendor' 

471 

472 

473class Branch(BaseModel): 

474 """ 

475 Is a part of the hierarchical structure of the product tree. 

476 """ 

477 

478 branches: Optional[Branches] = None 

479 category: Annotated[ 

480 BranchCategory, 

481 Field( 

482 description='Describes the characteristics of the labeled branch.', 

483 title='Category of the branch', 

484 ), 

485 ] 

486 name: Annotated[ 

487 str, 

488 Field( 

489 description="Contains the canonical descriptor or 'friendly name' of the branch.", 

490 examples=[ 

491 '10', 

492 '365', 

493 'Microsoft', 

494 'Office', 

495 'PCS 7', 

496 'SIMATIC', 

497 'Siemens', 

498 'Windows', 

499 ], 

500 min_length=1, 

501 title='Name of the branch', 

502 ), 

503 ] 

504 product: Optional[FullProductName] = None 

505 

506 

507class Branches( 

508 RootModel[ 

509 Annotated[ 

510 List[Branch], 

511 Field( 

512 description='Contains branch elements as children of the current element.', 

513 min_length=1, 

514 title='List of branches', 

515 ), 

516 ] 

517 ] 

518): 

519 """Contains branch elements as children of the current element.""" 

520 

521 @classmethod 

522 @no_type_check 

523 @model_validator(mode='before') 

524 def check_len(cls, v): 

525 if not v: 

526 raise ValueError('mandatory element present but empty') 

527 return v 

528 

529 

530Branch.model_rebuild() 

531FullProductName.model_rebuild() 

532ProductTree.model_rebuild()