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

127 statements  

« prev     ^ index     » next       coverage.py v7.0.3, created at 2023-01-07 19:14 +0100

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

2from __future__ import annotations 

3 

4import re 

5from collections.abc import Sequence 

6from enum import Enum 

7from typing import Annotated, List, Optional, no_type_check 

8 

9from pydantic import BaseModel, Field, validator 

10 

11from turvallisuusneuvonta.csaf.definitions import ( 

12 AnyUrl, 

13 Products, 

14 ReferenceTokenForProductGroupInstance, 

15 ReferenceTokenForProductInstance, 

16) 

17 

18 

19class FileHash(BaseModel): 

20 """ 

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

22 """ 

23 

24 algorithm: Annotated[ 

25 str, 

26 Field( 

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

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

29 min_length=1, 

30 title='Algorithm of the cryptographic hash', 

31 ), 

32 ] 

33 value: Annotated[ 

34 str, 

35 Field( 

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

37 examples=[ 

38 ( 

39 '37df33cb7464da5c7f077f4d56a32bc84987ec1d85b234537c1c1a4d4fc8d09d' 

40 'c29e2e762cb5203677bf849a2855a0283710f1f5fe1d6ce8d5ac85c645d0fcb3' 

41 ), 

42 '4775203615d9534a8bfca96a93dc8b461a489f69124a130d786b42204f3341cc', 

43 '9ea4c8200113d49d26505da0e02e2f49055dc078d1ad7a419b32e291c7afebbb84badfbd46dec42883bea0b2a1fa697c', 

44 ], 

45 min_length=32, 

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

47 title='Value of the cryptographic hash', 

48 ), 

49 ] 

50 

51 

52class CryptographicHashes(BaseModel): 

53 """ 

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

55 """ 

56 

57 file_hashes: Annotated[ 

58 Sequence[FileHash], 

59 Field( 

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

61 # min_items=1, 

62 title='List of file hashes', 

63 ), 

64 ] 

65 filename: Annotated[ 

66 str, 

67 Field( 

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

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

70 # min_length=1, 

71 title='Filename', 

72 ), 

73 ] 

74 

75 @no_type_check 

76 @validator('file_hashes', 'filename') 

77 @classmethod 

78 def check_len(cls, v): 

79 if not v: 

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

81 return v 

82 

83 

84class GenericUri(BaseModel): 

85 """ 

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

87 derived from a standard not yet supported. 

88 """ 

89 

90 namespace: Annotated[ 

91 AnyUrl, 

92 Field( 

93 description=( 

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

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

96 ), 

97 title='Namespace of the generic URI', 

98 ), 

99 ] 

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

101 

102 

103class SerialNumber(BaseModel): 

104 __root__: Annotated[ 

105 str, 

106 Field( 

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

108 min_length=1, 

109 title='Serial number', 

110 ), 

111 ] 

112 

113 

114class StockKeepingUnit(BaseModel): 

115 __root__: Annotated[ 

116 str, 

117 Field( 

118 description=( 

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

120 ' to identify the component.' 

121 ), 

122 min_length=1, 

123 title='Stock keeping unit', 

124 ), 

125 ] 

126 

127 

128class HelperToIdentifyTheProduct(BaseModel): 

129 """ 

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

131 """ 

132 

133 cpe: Annotated[ 

134 Optional[str], 

135 Field( 

136 description=( 

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

138 ' to this specification.' 

139 ), 

140 min_length=5, 

141 regex=( 

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

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

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

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

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

147 ), 

148 title='Common Platform Enumeration representation', 

149 ), 

150 ] = None 

151 hashes: Annotated[ 

152 Optional[Sequence[CryptographicHashes]], 

153 Field( 

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

155 # min_items=1, 

156 title='List of hashes', 

157 ), 

158 ] = None 

159 purl: Annotated[ 

160 Optional[AnyUrl], 

161 Field( 

162 description=( 

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

164 ' locating software packages external to this specification.' 

165 ), 

166 # min_length=7, 

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

168 title='package URL representation', 

169 ), 

170 ] = None 

171 sbom_urls: Annotated[ 

172 Optional[Sequence[AnyUrl]], 

173 Field( 

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

175 # min_items=1, 

176 title='List of SBOM URLs', 

177 ), 

178 ] = None 

179 serial_numbers: Annotated[ 

180 Optional[Sequence[SerialNumber]], 

181 Field( 

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

183 # min_items=1, 

184 title='List of serial numbers', 

185 ), 

186 ] = None 

187 skus: Annotated[ 

188 Optional[Sequence[StockKeepingUnit]], 

189 Field( 

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

191 # min_items=1, 

192 title='List of stock keeping units', 

193 ), 

194 ] = None 

195 x_generic_uris: Annotated[ 

196 Optional[Sequence[GenericUri]], 

197 Field( 

198 description=( 

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

200 ' a standard not yet supported.' 

201 ), 

202 # min_items=1, 

203 title='List of generic URIs', 

204 ), 

205 ] = None 

206 

207 @no_type_check 

208 @validator('hashes', 'sbom_urls', 'serial_numbers', 'skus', 'x_generic_uris') 

209 @classmethod 

210 def check_len(cls, v): 

211 if not v: 

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

213 return v 

214 

215 @no_type_check 

216 @validator('purl') 

217 @classmethod 

218 def check_purl(cls, v): 

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

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

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

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

223 return v 

224 

225 

226class FullProductName(BaseModel): 

227 """ 

228 Specifies information about the product and assigns the product_id. 

229 """ 

230 

231 name: Annotated[ 

232 str, 

233 Field( 

234 description=( 

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

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

237 ), 

238 examples=[ 

239 'Cisco AnyConnect Secure Mobility Client 2.3.185', 

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

241 ], 

242 min_length=1, 

243 title='Textual description of the product', 

244 ), 

245 ] 

246 product_id: ReferenceTokenForProductInstance 

247 product_identification_helper: Annotated[ 

248 Optional[HelperToIdentifyTheProduct], 

249 Field( 

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

251 title='Helper to identify the product', 

252 ), 

253 ] = None 

254 

255 

256class ProductGroup(BaseModel): 

257 """ 

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

259 a group of products with a single identifier. 

260 """ 

261 

262 group_id: ReferenceTokenForProductGroupInstance 

263 product_ids: Annotated[ 

264 Sequence[ReferenceTokenForProductInstance], 

265 Field( 

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

267 # min_items=2, 

268 title='List of Product IDs', 

269 ), 

270 ] 

271 summary: Annotated[ 

272 Optional[str], 

273 Field( 

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

275 examples=[ 

276 'Products supporting Modbus.', 

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

278 ], 

279 min_length=1, 

280 title='Summary of the product group', 

281 ), 

282 ] = None 

283 

284 @no_type_check 

285 @validator('product_ids') 

286 @classmethod 

287 def check_len(cls, v): 

288 if len(v) < 2: 

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

290 return v 

291 

292 

293class ProductStatus(BaseModel): 

294 """ 

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

296 to the current vulnerability. 

297 """ 

298 

299 first_affected: Annotated[ 

300 Optional[Products], 

301 Field( 

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

303 title='First affected', 

304 ), 

305 ] 

306 first_fixed: Annotated[ 

307 Optional[Products], 

308 Field( 

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

310 ' fixed versions.', 

311 title='First fixed', 

312 ), 

313 ] 

314 fixed: Annotated[ 

315 Optional[Products], 

316 Field( 

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

318 ' fixed versions.', 

319 title='Fixed', 

320 ), 

321 ] 

322 known_affected: Annotated[ 

323 Optional[Products], 

324 Field( 

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

326 title='Known affected', 

327 ), 

328 ] 

329 known_not_affected: Annotated[ 

330 Optional[Products], 

331 Field( 

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

333 title='Known not affected', 

334 ), 

335 ] 

336 last_affected: Annotated[ 

337 Optional[Products], 

338 Field( 

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

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

341 title='Last affected', 

342 ), 

343 ] 

344 recommended: Annotated[ 

345 Optional[Products], 

346 Field( 

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

348 ' fixing the vulnerability.', 

349 title='Recommended', 

350 ), 

351 ] 

352 under_investigation: Annotated[ 

353 Optional[Products], 

354 Field( 

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

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

357 ' of the document.', 

358 title='Under investigation', 

359 ), 

360 ] 

361 

362 

363class RelationshipCategory(Enum): 

364 """ 

365 Defines the category of relationship for the referenced component. 

366 """ 

367 

368 default_component_of = 'default_component_of' 

369 external_component_of = 'external_component_of' 

370 installed_on = 'installed_on' 

371 installed_with = 'installed_with' 

372 optional_component_of = 'optional_component_of' 

373 

374 

375class Relationship(BaseModel): 

376 """ 

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

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

379 """ 

380 

381 category: Annotated[ 

382 RelationshipCategory, 

383 Field( 

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

385 title='Relationship category', 

386 ), 

387 ] 

388 full_product_name: FullProductName 

389 product_reference: Annotated[ 

390 ReferenceTokenForProductInstance, 

391 Field( 

392 description=( 

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

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

395 ), 

396 title='Product reference', 

397 ), 

398 ] 

399 relates_to_product_reference: Annotated[ 

400 ReferenceTokenForProductInstance, 

401 Field( 

402 description=( 

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

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

405 ), 

406 title='Relates to product reference', 

407 ), 

408 ] 

409 

410 

411class ProductTree(BaseModel): 

412 """ 

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

414 """ 

415 

416 branches: Optional[Branches] 

417 full_product_names: Annotated[ 

418 Optional[List[FullProductName]], 

419 Field( 

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

421 min_items=1, 

422 title='List of full product names', 

423 ), 

424 ] 

425 product_groups: Annotated[ 

426 Optional[List[ProductGroup]], 

427 Field( 

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

429 min_items=1, 

430 title='List of product groups', 

431 ), 

432 ] 

433 relationships: Annotated[ 

434 Optional[List[Relationship]], 

435 Field( 

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

437 min_items=1, 

438 title='List of relationships', 

439 ), 

440 ] 

441 

442 @no_type_check 

443 @validator('full_product_names', 'product_groups', 'relationships') 

444 @classmethod 

445 def check_len(cls, v): 

446 if not v: 

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

448 return v 

449 

450 

451class BranchCategory(Enum): 

452 """ 

453 Describes the characteristics of the labeled branch. 

454 """ 

455 

456 architecture = 'architecture' 

457 host_name = 'host_name' 

458 language = 'language' 

459 legacy = 'legacy' 

460 patch_level = 'patch_level' 

461 product_family = 'product_family' 

462 product_name = 'product_name' 

463 product_version = 'product_version' 

464 service_pack = 'service_pack' 

465 specification = 'specification' 

466 vendor = 'vendor' 

467 

468 

469class Branch(BaseModel): 

470 """ 

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

472 """ 

473 

474 branches: Optional[Branches] 

475 category: Annotated[ 

476 BranchCategory, 

477 Field( 

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

479 title='Category of the branch', 

480 ), 

481 ] 

482 name: Annotated[ 

483 str, 

484 Field( 

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

486 examples=[ 

487 '10', 

488 '365', 

489 'Microsoft', 

490 'Office', 

491 'PCS 7', 

492 'SIMATIC', 

493 'Siemens', 

494 'Windows', 

495 ], 

496 min_length=1, 

497 title='Name of the branch', 

498 ), 

499 ] 

500 product: Optional[FullProductName] 

501 

502 

503class Branches(BaseModel): 

504 """ 

505 Contains branch elements as children of the current element. 

506 """ 

507 

508 __root__: Annotated[ 

509 List[Branch], 

510 Field( 

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

512 min_items=1, 

513 title='List of branches', 

514 ), 

515 ] 

516 

517 @no_type_check 

518 @validator('__root__') 

519 @classmethod 

520 def check_len(cls, v): 

521 if not v: 

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

523 return v 

524 

525 

526Branch.update_forward_refs() 

527FullProductName.update_forward_refs() 

528ProductTree.update_forward_refs()