Coverage for csaf/product.py: 76.55%

131 statements  

« prev     ^ index     » next       coverage.py v7.6.9, created at 2024-12-18 20:12:48 +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, ConfigDict, 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 ModelNumber( 

100 RootModel[ 

101 Annotated[ 

102 str, 

103 Field( 

104 description='Contains a full or abbreviated (partial) model number of the component to identify.', 

105 min_length=1, 

106 title='Model number', 

107 ), 

108 ] 

109 ] 

110): 

111 pass 

112 

113 

114class SerialNumber( 

115 RootModel[ 

116 Annotated[ 

117 str, 

118 Field( 

119 description='Contains a full or abbreviated (partial) serial number of the component to identify.', 

120 min_length=1, 

121 title='Serial number', 

122 ), 

123 ] 

124 ] 

125): 

126 pass 

127 

128 

129class StockKeepingUnit( 

130 RootModel[ 

131 Annotated[ 

132 str, 

133 Field( 

134 description=( 

135 'Contains a full or abbreviated (partial) stock keeping unit (SKU) which is used in' 

136 ' the ordering process to identify the component.' 

137 ), 

138 min_length=1, 

139 title='Stock keeping unit', 

140 ), 

141 ] 

142 ] 

143): 

144 pass 

145 

146 

147class HelperToIdentifyTheProduct(BaseModel): 

148 """ 

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

150 """ 

151 

152 model_config = ConfigDict(protected_namespaces=()) 

153 cpe: Annotated[ 

154 Optional[str], 

155 Field( 

156 description=( 

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

158 ' to this specification.' 

159 ), 

160 min_length=5, 

161 pattern=( 

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

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

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

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

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

167 ), 

168 title='Common Platform Enumeration representation', 

169 ), 

170 ] = None 

171 hashes: Annotated[ 

172 Optional[Sequence[CryptographicHashes]], 

173 Field( 

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

175 # min_items=1, 

176 title='List of hashes', 

177 ), 

178 ] = None 

179 purl: Annotated[ 

180 Optional[AnyUrl], 

181 Field( 

182 description=( 

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

184 ' locating software packages external to this specification.' 

185 ), 

186 # min_length=7, 

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

188 title='package URL representation', 

189 ), 

190 ] = None 

191 sbom_urls: Annotated[ 

192 Optional[Sequence[AnyUrl]], 

193 Field( 

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

195 # min_items=1, 

196 title='List of SBOM URLs', 

197 ), 

198 ] = None 

199 serial_numbers: Annotated[ 

200 Optional[Sequence[SerialNumber]], 

201 Field( 

202 description='Contains a list of full or abbreviated (partial) serial numbers.', 

203 # min_items=1, 

204 # unique_items=True, 

205 title='List of serial numbers', 

206 ), 

207 ] = None 

208 model_numbers: Annotated[ 

209 Optional[Sequence[ModelNumber]], 

210 Field( 

211 alias='model_numbers', 

212 description='Contains a list of full or abbreviated (partial) model numbers.', 

213 # min_items=1, 

214 title='List of model numbers', 

215 ), 

216 ] = None 

217 skus: Annotated[ 

218 Optional[Sequence[StockKeepingUnit]], 

219 Field( 

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

221 # min_items=1, 

222 title='List of stock keeping units', 

223 ), 

224 ] = None 

225 x_generic_uris: Annotated[ 

226 Optional[Sequence[GenericUri]], 

227 Field( 

228 description=( 

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

230 ' a standard not yet supported.' 

231 ), 

232 # min_items=1, 

233 title='List of generic URIs', 

234 ), 

235 ] = None 

236 

237 @classmethod 

238 @no_type_check 

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

240 def check_len(cls, v): 

241 if not v: 

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

243 return v 

244 

245 @classmethod 

246 @no_type_check 

247 @field_validator('purl') 

248 def check_purl(cls, v): 

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

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

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

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

253 return v 

254 

255 

256class FullProductName(BaseModel): 

257 """ 

258 Specifies information about the product and assigns the product_id. 

259 """ 

260 

261 name: Annotated[ 

262 str, 

263 Field( 

264 description=( 

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

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

267 ), 

268 examples=[ 

269 'Cisco AnyConnect Secure Mobility Client 2.3.185', 

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

271 ], 

272 min_length=1, 

273 title='Textual description of the product', 

274 ), 

275 ] 

276 product_id: ReferenceTokenForProductInstance 

277 product_identification_helper: Annotated[ 

278 Optional[HelperToIdentifyTheProduct], 

279 Field( 

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

281 title='Helper to identify the product', 

282 ), 

283 ] = None 

284 

285 

286class ProductGroup(BaseModel): 

287 """ 

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

289 a group of products with a single identifier. 

290 """ 

291 

292 group_id: ReferenceTokenForProductGroupInstance 

293 product_ids: Annotated[ 

294 Sequence[ReferenceTokenForProductInstance], 

295 Field( 

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

297 # min_items=2, 

298 title='List of Product IDs', 

299 ), 

300 ] 

301 summary: Annotated[ 

302 Optional[str], 

303 Field( 

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

305 examples=[ 

306 'Products supporting Modbus.', 

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

308 ], 

309 min_length=1, 

310 title='Summary of the product group', 

311 ), 

312 ] = None 

313 

314 @classmethod 

315 @no_type_check 

316 @field_validator('product_ids') 

317 def check_len(cls, v): 

318 if len(v) < 2: 

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

320 return v 

321 

322 

323class ProductStatus(BaseModel): 

324 """ 

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

326 to the current vulnerability. 

327 """ 

328 

329 first_affected: Annotated[ 

330 Optional[Products], 

331 Field( 

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

333 title='First affected', 

334 ), 

335 ] = None 

336 first_fixed: Annotated[ 

337 Optional[Products], 

338 Field( 

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

340 ' fixed versions.', 

341 title='First fixed', 

342 ), 

343 ] = None 

344 fixed: Annotated[ 

345 Optional[Products], 

346 Field( 

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

348 ' fixed versions.', 

349 title='Fixed', 

350 ), 

351 ] = None 

352 known_affected: Annotated[ 

353 Optional[Products], 

354 Field( 

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

356 title='Known affected', 

357 ), 

358 ] = None 

359 known_not_affected: Annotated[ 

360 Optional[Products], 

361 Field( 

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

363 title='Known not affected', 

364 ), 

365 ] = None 

366 last_affected: Annotated[ 

367 Optional[Products], 

368 Field( 

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

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

371 title='Last affected', 

372 ), 

373 ] = None 

374 recommended: Annotated[ 

375 Optional[Products], 

376 Field( 

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

378 ' fixing the vulnerability.', 

379 title='Recommended', 

380 ), 

381 ] = None 

382 under_investigation: Annotated[ 

383 Optional[Products], 

384 Field( 

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

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

387 ' of the document.', 

388 title='Under investigation', 

389 ), 

390 ] = None 

391 

392 

393class RelationshipCategory(Enum): 

394 """ 

395 Defines the category of relationship for the referenced component. 

396 """ 

397 

398 default_component_of = 'default_component_of' 

399 external_component_of = 'external_component_of' 

400 installed_on = 'installed_on' 

401 installed_with = 'installed_with' 

402 optional_component_of = 'optional_component_of' 

403 

404 

405class Relationship(BaseModel): 

406 """ 

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

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

409 """ 

410 

411 category: Annotated[ 

412 RelationshipCategory, 

413 Field( 

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

415 title='Relationship category', 

416 ), 

417 ] 

418 full_product_name: FullProductName 

419 product_reference: Annotated[ 

420 ReferenceTokenForProductInstance, 

421 Field( 

422 description=( 

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

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

425 ), 

426 title='Product reference', 

427 ), 

428 ] 

429 relates_to_product_reference: Annotated[ 

430 ReferenceTokenForProductInstance, 

431 Field( 

432 description=( 

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

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

435 ), 

436 title='Relates to product reference', 

437 ), 

438 ] 

439 

440 

441class ProductTree(BaseModel): 

442 """ 

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

444 """ 

445 

446 branches: Optional[Branches] = None 

447 full_product_names: Annotated[ 

448 Optional[List[FullProductName]], 

449 Field( 

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

451 min_length=1, 

452 title='List of full product names', 

453 ), 

454 ] = None 

455 product_groups: Annotated[ 

456 Optional[List[ProductGroup]], 

457 Field( 

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

459 min_length=1, 

460 title='List of product groups', 

461 ), 

462 ] = None 

463 relationships: Annotated[ 

464 Optional[List[Relationship]], 

465 Field( 

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

467 min_length=1, 

468 title='List of relationships', 

469 ), 

470 ] = None 

471 

472 @classmethod 

473 @no_type_check 

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

475 def check_len(cls, v): 

476 if not v: 

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

478 return v 

479 

480 

481class BranchCategory(Enum): 

482 """ 

483 Describes the characteristics of the labeled branch. 

484 """ 

485 

486 architecture = 'architecture' 

487 host_name = 'host_name' 

488 language = 'language' 

489 legacy = 'legacy' 

490 patch_level = 'patch_level' 

491 product_family = 'product_family' 

492 product_name = 'product_name' 

493 product_version = 'product_version' 

494 product_version_range = 'product_version_range' 

495 service_pack = 'service_pack' 

496 specification = 'specification' 

497 vendor = 'vendor' 

498 

499 

500class Branch(BaseModel): 

501 """ 

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

503 """ 

504 

505 branches: Optional[Branches] = None 

506 category: Annotated[ 

507 BranchCategory, 

508 Field( 

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

510 title='Category of the branch', 

511 ), 

512 ] 

513 name: Annotated[ 

514 str, 

515 Field( 

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

517 examples=[ 

518 '10', 

519 '365', 

520 'Microsoft', 

521 'Office', 

522 'PCS 7', 

523 'SIMATIC', 

524 'Siemens', 

525 'Windows', 

526 ], 

527 min_length=1, 

528 title='Name of the branch', 

529 ), 

530 ] 

531 product: Optional[FullProductName] = None 

532 

533 

534class Branches( 

535 RootModel[ 

536 Annotated[ 

537 List[Branch], 

538 Field( 

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

540 min_length=1, 

541 title='List of branches', 

542 ), 

543 ] 

544 ] 

545): 

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

547 

548 @classmethod 

549 @no_type_check 

550 @model_validator(mode='before') 

551 def check_len(cls, v): 

552 if not v: 

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

554 return v 

555 

556 

557Branch.model_rebuild() 

558FullProductName.model_rebuild() 

559ProductTree.model_rebuild()