Coverage for turvallisuusneuvonta/csaf/product.py: 76.55%

131 statements  

« prev     ^ index     » next       coverage.py v7.6.9, created at 2024-12-18 20:29:38 +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 turvallisuusneuvonta.csaf.definitions import ( 

13 AnyUrl, 

14 Products, 

15 ReferenceTokenForProductGroupInstance, 

16 ReferenceTokenForProductInstance, 

17) 

18 

19 

20class FileHash(BaseModel): 

21 """ 

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

23 """ 

24 

25 algorithm: Annotated[ 

26 str, 

27 Field( 

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

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

30 min_length=1, 

31 title='Algorithm of the cryptographic hash', 

32 ), 

33 ] 

34 value: Annotated[ 

35 str, 

36 Field( 

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

38 examples=[ 

39 ( 

40 '37df33cb7464da5c7f077f4d56a32bc84987ec1d85b234537c1c1a4d4fc8d09d' 

41 'c29e2e762cb5203677bf849a2855a0283710f1f5fe1d6ce8d5ac85c645d0fcb3' 

42 ), 

43 '4775203615d9534a8bfca96a93dc8b461a489f69124a130d786b42204f3341cc', 

44 '9ea4c8200113d49d26505da0e02e2f49055dc078d1ad7a419b32e291c7afebbb84badfbd46dec42883bea0b2a1fa697c', 

45 ], 

46 min_length=32, 

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

48 title='Value of the cryptographic hash', 

49 ), 

50 ] 

51 

52 

53class CryptographicHashes(BaseModel): 

54 """ 

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

56 """ 

57 

58 file_hashes: Annotated[ 

59 Sequence[FileHash], 

60 Field( 

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

62 # min_length=1, 

63 title='List of file hashes', 

64 ), 

65 ] 

66 filename: Annotated[ 

67 str, 

68 Field( 

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

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

71 # min_length=1, 

72 title='Filename', 

73 ), 

74 ] 

75 

76 @classmethod 

77 @no_type_check 

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

79 def check_len(cls, v): 

80 if not v: 

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

82 return v 

83 

84 

85class GenericUri(BaseModel): 

86 """ 

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

88 derived from a standard not yet supported. 

89 """ 

90 

91 namespace: Annotated[ 

92 AnyUrl, 

93 Field( 

94 description=( 

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

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

97 ), 

98 title='Namespace of the generic URI', 

99 ), 

100 ] 

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

102 

103 

104class ModelNumber( 

105 RootModel[ 

106 Annotated[ 

107 str, 

108 Field( 

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

110 min_length=1, 

111 title='Model number', 

112 ), 

113 ] 

114 ] 

115): 

116 pass 

117 

118 

119class SerialNumber( 

120 RootModel[ 

121 Annotated[ 

122 str, 

123 Field( 

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

125 min_length=1, 

126 title='Serial number', 

127 ), 

128 ] 

129 ] 

130): 

131 pass 

132 

133 

134class StockKeepingUnit( 

135 RootModel[ 

136 Annotated[ 

137 str, 

138 Field( 

139 description=( 

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

141 ' the ordering process to identify the component.' 

142 ), 

143 min_length=1, 

144 title='Stock keeping unit', 

145 ), 

146 ] 

147 ] 

148): 

149 pass 

150 

151 

152class HelperToIdentifyTheProduct(BaseModel): 

153 """ 

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

155 """ 

156 

157 model_config = ConfigDict(protected_namespaces=()) 

158 cpe: Annotated[ 

159 Optional[str], 

160 Field( 

161 description=( 

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

163 ' to this specification.' 

164 ), 

165 min_length=5, 

166 pattern=( 

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

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

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

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

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

172 ), 

173 title='Common Platform Enumeration representation', 

174 ), 

175 ] = None 

176 hashes: Annotated[ 

177 Optional[Sequence[CryptographicHashes]], 

178 Field( 

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

180 # min_length=1, 

181 title='List of hashes', 

182 ), 

183 ] = None 

184 model_numbers: Annotated[ 

185 Optional[Sequence[ModelNumber]], 

186 Field( 

187 alias='model_numbers', 

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

189 # min_items=1, 

190 title='List of model numbers', 

191 ), 

192 ] = None 

193 purl: Annotated[ 

194 Optional[AnyUrl], 

195 Field( 

196 description=( 

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

198 ' locating software packages external to this specification.' 

199 ), 

200 # min_length=7, 

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

202 title='package URL representation', 

203 ), 

204 ] = None 

205 sbom_urls: Annotated[ 

206 Optional[Sequence[AnyUrl]], 

207 Field( 

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

209 # min_length=1, 

210 title='List of SBOM URLs', 

211 ), 

212 ] = None 

213 serial_numbers: Annotated[ 

214 Optional[Sequence[SerialNumber]], 

215 Field( 

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

217 # min_length=1, 

218 # # unique_items=True, 

219 title='List of serial numbers', 

220 ), 

221 ] = None 

222 skus: Annotated[ 

223 Optional[Sequence[StockKeepingUnit]], 

224 Field( 

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

226 # min_length=1, 

227 title='List of stock keeping units', 

228 ), 

229 ] = None 

230 x_generic_uris: Annotated[ 

231 Optional[Sequence[GenericUri]], 

232 Field( 

233 description=( 

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

235 ' a standard not yet supported.' 

236 ), 

237 # min_length=1, 

238 title='List of generic URIs', 

239 ), 

240 ] = None 

241 

242 @classmethod 

243 @no_type_check 

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

245 def check_len(cls, v): 

246 if not v: 

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

248 return v 

249 

250 @classmethod 

251 @no_type_check 

252 @field_validator('purl') 

253 def check_purl(cls, v): 

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

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

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

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

258 return v 

259 

260 

261class FullProductName(BaseModel): 

262 """ 

263 Specifies information about the product and assigns the product_id. 

264 """ 

265 

266 name: Annotated[ 

267 str, 

268 Field( 

269 description=( 

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

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

272 ), 

273 examples=[ 

274 'Cisco AnyConnect Secure Mobility Client 2.3.185', 

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

276 ], 

277 min_length=1, 

278 title='Textual description of the product', 

279 ), 

280 ] 

281 product_id: ReferenceTokenForProductInstance 

282 product_identification_helper: Annotated[ 

283 Optional[HelperToIdentifyTheProduct], 

284 Field( 

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

286 title='Helper to identify the product', 

287 ), 

288 ] = None 

289 

290 

291class ProductGroup(BaseModel): 

292 """ 

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

294 a group of products with a single identifier. 

295 """ 

296 

297 group_id: ReferenceTokenForProductGroupInstance 

298 product_ids: Annotated[ 

299 Sequence[ReferenceTokenForProductInstance], 

300 Field( 

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

302 # min_length=2, 

303 title='List of Product IDs', 

304 ), 

305 ] 

306 summary: Annotated[ 

307 Optional[str], 

308 Field( 

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

310 examples=[ 

311 'Products supporting Modbus.', 

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

313 ], 

314 min_length=1, 

315 title='Summary of the product group', 

316 ), 

317 ] = None 

318 

319 @classmethod 

320 @no_type_check 

321 @field_validator('product_ids') 

322 def check_len(cls, v): 

323 if len(v) < 2: 

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

325 return v 

326 

327 

328class ProductStatus(BaseModel): 

329 """ 

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

331 to the current vulnerability. 

332 """ 

333 

334 first_affected: Annotated[ 

335 Optional[Products], 

336 Field( 

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

338 title='First affected', 

339 ), 

340 ] = None 

341 first_fixed: Annotated[ 

342 Optional[Products], 

343 Field( 

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

345 ' fixed versions.', 

346 title='First fixed', 

347 ), 

348 ] = None 

349 fixed: Annotated[ 

350 Optional[Products], 

351 Field( 

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

353 ' fixed versions.', 

354 title='Fixed', 

355 ), 

356 ] = None 

357 known_affected: Annotated[ 

358 Optional[Products], 

359 Field( 

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

361 title='Known affected', 

362 ), 

363 ] = None 

364 known_not_affected: Annotated[ 

365 Optional[Products], 

366 Field( 

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

368 title='Known not affected', 

369 ), 

370 ] = None 

371 last_affected: Annotated[ 

372 Optional[Products], 

373 Field( 

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

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

376 title='Last affected', 

377 ), 

378 ] = None 

379 recommended: Annotated[ 

380 Optional[Products], 

381 Field( 

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

383 ' fixing the vulnerability.', 

384 title='Recommended', 

385 ), 

386 ] = None 

387 under_investigation: Annotated[ 

388 Optional[Products], 

389 Field( 

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

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

392 ' of the document.', 

393 title='Under investigation', 

394 ), 

395 ] = None 

396 

397 

398class RelationshipCategory(Enum): 

399 """ 

400 Defines the category of relationship for the referenced component. 

401 """ 

402 

403 default_component_of = 'default_component_of' 

404 external_component_of = 'external_component_of' 

405 installed_on = 'installed_on' 

406 installed_with = 'installed_with' 

407 optional_component_of = 'optional_component_of' 

408 

409 

410class Relationship(BaseModel): 

411 """ 

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

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

414 """ 

415 

416 category: Annotated[ 

417 RelationshipCategory, 

418 Field( 

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

420 title='Relationship category', 

421 ), 

422 ] 

423 full_product_name: FullProductName 

424 product_reference: Annotated[ 

425 ReferenceTokenForProductInstance, 

426 Field( 

427 description=( 

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

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

430 ), 

431 title='Product reference', 

432 ), 

433 ] 

434 relates_to_product_reference: Annotated[ 

435 ReferenceTokenForProductInstance, 

436 Field( 

437 description=( 

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

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

440 ), 

441 title='Relates to product reference', 

442 ), 

443 ] 

444 

445 

446class ProductTree(BaseModel): 

447 """ 

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

449 """ 

450 

451 branches: Optional[Branches] = None 

452 full_product_names: Annotated[ 

453 Optional[List[FullProductName]], 

454 Field( 

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

456 min_length=1, 

457 title='List of full product names', 

458 ), 

459 ] = None 

460 product_groups: Annotated[ 

461 Optional[List[ProductGroup]], 

462 Field( 

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

464 min_length=1, 

465 title='List of product groups', 

466 ), 

467 ] = None 

468 relationships: Annotated[ 

469 Optional[List[Relationship]], 

470 Field( 

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

472 min_length=1, 

473 title='List of relationships', 

474 ), 

475 ] = None 

476 

477 @classmethod 

478 @no_type_check 

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

480 def check_len(cls, v): 

481 if not v: 

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

483 return v 

484 

485 

486class BranchCategory(Enum): 

487 """ 

488 Describes the characteristics of the labeled branch. 

489 """ 

490 

491 architecture = 'architecture' 

492 host_name = 'host_name' 

493 language = 'language' 

494 legacy = 'legacy' 

495 patch_level = 'patch_level' 

496 product_family = 'product_family' 

497 product_name = 'product_name' 

498 product_version = 'product_version' 

499 product_version_range = 'product_version_range' 

500 service_pack = 'service_pack' 

501 specification = 'specification' 

502 vendor = 'vendor' 

503 

504 

505class Branch(BaseModel): 

506 """ 

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

508 """ 

509 

510 branches: Optional[Branches] 

511 category: Annotated[ 

512 BranchCategory, 

513 Field( 

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

515 title='Category of the branch', 

516 ), 

517 ] 

518 name: Annotated[ 

519 str, 

520 Field( 

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

522 examples=[ 

523 '10', 

524 '365', 

525 'Microsoft', 

526 'Office', 

527 'PCS 7', 

528 'SIMATIC', 

529 'Siemens', 

530 'Windows', 

531 ], 

532 min_length=1, 

533 title='Name of the branch', 

534 ), 

535 ] 

536 product: Optional[FullProductName] = None 

537 

538 

539class Branches( 

540 RootModel[ 

541 Annotated[ 

542 List[Branch], 

543 Field( 

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

545 min_length=1, 

546 title='List of branches', 

547 ), 

548 ] 

549 ] 

550): 

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

552 

553 @classmethod 

554 @no_type_check 

555 @model_validator(mode='before') 

556 def check_len(cls, v): 

557 if not v: 

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

559 return v 

560 

561 

562Branch.model_rebuild() 

563FullProductName.model_rebuild() 

564ProductTree.model_rebuild()