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

126 statements  

« prev     ^ index     » next       coverage.py v7.4.1, created at 2024-02-05 19:27:17 +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 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 SerialNumber( 

105 RootModel[ 

106 Annotated[ 

107 str, 

108 Field( 

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

110 min_length=1, 

111 title='Serial number', 

112 ), 

113 ] 

114 ] 

115): 

116 pass 

117 

118 

119class StockKeepingUnit( 

120 RootModel[ 

121 Annotated[ 

122 str, 

123 Field( 

124 description=( 

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

126 ' to identify the component.' 

127 ), 

128 min_length=1, 

129 title='Stock keeping unit', 

130 ), 

131 ] 

132 ] 

133): 

134 pass 

135 

136 

137class HelperToIdentifyTheProduct(BaseModel): 

138 """ 

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

140 """ 

141 

142 cpe: Annotated[ 

143 Optional[str], 

144 Field( 

145 description=( 

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

147 ' to this specification.' 

148 ), 

149 min_length=5, 

150 pattern=( 

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

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

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

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

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

156 ), 

157 title='Common Platform Enumeration representation', 

158 ), 

159 ] = None 

160 hashes: Annotated[ 

161 Optional[Sequence[CryptographicHashes]], 

162 Field( 

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

164 # min_length=1, 

165 title='List of hashes', 

166 ), 

167 ] = None 

168 purl: Annotated[ 

169 Optional[AnyUrl], 

170 Field( 

171 description=( 

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

173 ' locating software packages external to this specification.' 

174 ), 

175 # min_length=7, 

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

177 title='package URL representation', 

178 ), 

179 ] = None 

180 sbom_urls: Annotated[ 

181 Optional[Sequence[AnyUrl]], 

182 Field( 

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

184 # min_length=1, 

185 title='List of SBOM URLs', 

186 ), 

187 ] = None 

188 serial_numbers: Annotated[ 

189 Optional[Sequence[SerialNumber]], 

190 Field( 

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

192 # min_length=1, 

193 title='List of serial numbers', 

194 ), 

195 ] = None 

196 skus: Annotated[ 

197 Optional[Sequence[StockKeepingUnit]], 

198 Field( 

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

200 # min_length=1, 

201 title='List of stock keeping units', 

202 ), 

203 ] = None 

204 x_generic_uris: Annotated[ 

205 Optional[Sequence[GenericUri]], 

206 Field( 

207 description=( 

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

209 ' a standard not yet supported.' 

210 ), 

211 # min_length=1, 

212 title='List of generic URIs', 

213 ), 

214 ] = None 

215 

216 @classmethod 

217 @no_type_check 

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

219 def check_len(cls, v): 

220 if not v: 

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

222 return v 

223 

224 @classmethod 

225 @no_type_check 

226 @field_validator('purl') 

227 def check_purl(cls, v): 

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

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

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

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

232 return v 

233 

234 

235class FullProductName(BaseModel): 

236 """ 

237 Specifies information about the product and assigns the product_id. 

238 """ 

239 

240 name: Annotated[ 

241 str, 

242 Field( 

243 description=( 

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

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

246 ), 

247 examples=[ 

248 'Cisco AnyConnect Secure Mobility Client 2.3.185', 

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

250 ], 

251 min_length=1, 

252 title='Textual description of the product', 

253 ), 

254 ] 

255 product_id: ReferenceTokenForProductInstance 

256 product_identification_helper: Annotated[ 

257 Optional[HelperToIdentifyTheProduct], 

258 Field( 

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

260 title='Helper to identify the product', 

261 ), 

262 ] = None 

263 

264 

265class ProductGroup(BaseModel): 

266 """ 

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

268 a group of products with a single identifier. 

269 """ 

270 

271 group_id: ReferenceTokenForProductGroupInstance 

272 product_ids: Annotated[ 

273 Sequence[ReferenceTokenForProductInstance], 

274 Field( 

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

276 # min_length=2, 

277 title='List of Product IDs', 

278 ), 

279 ] 

280 summary: Annotated[ 

281 Optional[str], 

282 Field( 

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

284 examples=[ 

285 'Products supporting Modbus.', 

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

287 ], 

288 min_length=1, 

289 title='Summary of the product group', 

290 ), 

291 ] = None 

292 

293 @classmethod 

294 @no_type_check 

295 @field_validator('product_ids') 

296 def check_len(cls, v): 

297 if len(v) < 2: 

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

299 return v 

300 

301 

302class ProductStatus(BaseModel): 

303 """ 

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

305 to the current vulnerability. 

306 """ 

307 

308 first_affected: Annotated[ 

309 Optional[Products], 

310 Field( 

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

312 title='First affected', 

313 ), 

314 ] = None 

315 first_fixed: Annotated[ 

316 Optional[Products], 

317 Field( 

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

319 ' fixed versions.', 

320 title='First fixed', 

321 ), 

322 ] = None 

323 fixed: Annotated[ 

324 Optional[Products], 

325 Field( 

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

327 ' fixed versions.', 

328 title='Fixed', 

329 ), 

330 ] = None 

331 known_affected: Annotated[ 

332 Optional[Products], 

333 Field( 

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

335 title='Known affected', 

336 ), 

337 ] = None 

338 known_not_affected: Annotated[ 

339 Optional[Products], 

340 Field( 

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

342 title='Known not affected', 

343 ), 

344 ] = None 

345 last_affected: Annotated[ 

346 Optional[Products], 

347 Field( 

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

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

350 title='Last affected', 

351 ), 

352 ] = None 

353 recommended: Annotated[ 

354 Optional[Products], 

355 Field( 

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

357 ' fixing the vulnerability.', 

358 title='Recommended', 

359 ), 

360 ] = None 

361 under_investigation: Annotated[ 

362 Optional[Products], 

363 Field( 

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

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

366 ' of the document.', 

367 title='Under investigation', 

368 ), 

369 ] = None 

370 

371 

372class RelationshipCategory(Enum): 

373 """ 

374 Defines the category of relationship for the referenced component. 

375 """ 

376 

377 default_component_of = 'default_component_of' 

378 external_component_of = 'external_component_of' 

379 installed_on = 'installed_on' 

380 installed_with = 'installed_with' 

381 optional_component_of = 'optional_component_of' 

382 

383 

384class Relationship(BaseModel): 

385 """ 

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

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

388 """ 

389 

390 category: Annotated[ 

391 RelationshipCategory, 

392 Field( 

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

394 title='Relationship category', 

395 ), 

396 ] 

397 full_product_name: FullProductName 

398 product_reference: Annotated[ 

399 ReferenceTokenForProductInstance, 

400 Field( 

401 description=( 

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

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

404 ), 

405 title='Product reference', 

406 ), 

407 ] 

408 relates_to_product_reference: Annotated[ 

409 ReferenceTokenForProductInstance, 

410 Field( 

411 description=( 

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

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

414 ), 

415 title='Relates to product reference', 

416 ), 

417 ] 

418 

419 

420class ProductTree(BaseModel): 

421 """ 

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

423 """ 

424 

425 branches: Optional[Branches] = None 

426 full_product_names: Annotated[ 

427 Optional[List[FullProductName]], 

428 Field( 

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

430 min_length=1, 

431 title='List of full product names', 

432 ), 

433 ] = None 

434 product_groups: Annotated[ 

435 Optional[List[ProductGroup]], 

436 Field( 

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

438 min_length=1, 

439 title='List of product groups', 

440 ), 

441 ] = None 

442 relationships: Annotated[ 

443 Optional[List[Relationship]], 

444 Field( 

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

446 min_length=1, 

447 title='List of relationships', 

448 ), 

449 ] = None 

450 

451 @classmethod 

452 @no_type_check 

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

454 def check_len(cls, v): 

455 if not v: 

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

457 return v 

458 

459 

460class BranchCategory(Enum): 

461 """ 

462 Describes the characteristics of the labeled branch. 

463 """ 

464 

465 architecture = 'architecture' 

466 host_name = 'host_name' 

467 language = 'language' 

468 legacy = 'legacy' 

469 patch_level = 'patch_level' 

470 product_family = 'product_family' 

471 product_name = 'product_name' 

472 product_version = 'product_version' 

473 service_pack = 'service_pack' 

474 specification = 'specification' 

475 vendor = 'vendor' 

476 

477 

478class Branch(BaseModel): 

479 """ 

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

481 """ 

482 

483 branches: Optional[Branches] 

484 category: Annotated[ 

485 BranchCategory, 

486 Field( 

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

488 title='Category of the branch', 

489 ), 

490 ] 

491 name: Annotated[ 

492 str, 

493 Field( 

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

495 examples=[ 

496 '10', 

497 '365', 

498 'Microsoft', 

499 'Office', 

500 'PCS 7', 

501 'SIMATIC', 

502 'Siemens', 

503 'Windows', 

504 ], 

505 min_length=1, 

506 title='Name of the branch', 

507 ), 

508 ] 

509 product: Optional[FullProductName] = None 

510 

511 

512class Branches( 

513 RootModel[ 

514 Annotated[ 

515 List[Branch], 

516 Field( 

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

518 min_length=1, 

519 title='List of branches', 

520 ), 

521 ] 

522 ] 

523): 

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

525 

526 @classmethod 

527 @no_type_check 

528 @model_validator(mode='before') 

529 def check_len(cls, v): 

530 if not v: 

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

532 return v 

533 

534 

535Branch.model_rebuild() 

536FullProductName.model_rebuild() 

537ProductTree.model_rebuild()