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
« prev ^ index » next coverage.py v7.6.9, created at 2024-12-18 20:12:48 +00:00
1"""CSAF Product Tree model."""
3from __future__ import annotations
5import re
6from collections.abc import Sequence
7from enum import Enum
8from typing import Annotated, List, Optional, no_type_check
10from pydantic import BaseModel, ConfigDict, Field, RootModel, field_validator, model_validator
12from csaf.definitions import AnyUrl, Products, ReferenceTokenForProductGroupInstance, ReferenceTokenForProductInstance
15class FileHash(BaseModel):
16 """
17 Contains one hash value and algorithm of the file to be identified.
18 """
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 ]
48class CryptographicHashes(BaseModel):
49 """
50 Contains all information to identify a file based on its cryptographic hash values.
51 """
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 ]
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
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 """
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')]
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
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
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
147class HelperToIdentifyTheProduct(BaseModel):
148 """
149 Provides at least one method which aids in identifying the product in an asset database.
150 """
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
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
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
256class FullProductName(BaseModel):
257 """
258 Specifies information about the product and assigns the product_id.
259 """
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
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 """
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
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
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 """
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
393class RelationshipCategory(Enum):
394 """
395 Defines the category of relationship for the referenced component.
396 """
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'
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 """
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 ]
441class ProductTree(BaseModel):
442 """
443 Is a container for all fully qualified product names that can be referenced elsewhere in the document.
444 """
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
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
481class BranchCategory(Enum):
482 """
483 Describes the characteristics of the labeled branch.
484 """
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'
500class Branch(BaseModel):
501 """
502 Is a part of the hierarchical structure of the product tree.
503 """
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
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."""
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
557Branch.model_rebuild()
558FullProductName.model_rebuild()
559ProductTree.model_rebuild()