Coverage for csaf/product.py: 81.91%
126 statements
« prev ^ index » next coverage.py v7.4.1, created at 2024-02-04 16:28:45 +00:00
« prev ^ index » next coverage.py v7.4.1, created at 2024-02-04 16:28:45 +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, 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 SerialNumber(
100 RootModel[
101 Annotated[
102 str,
103 Field(
104 description='Contains a part, or a full serial number of the component to identify.',
105 min_length=1,
106 title='Serial number',
107 ),
108 ]
109 ]
110):
111 pass
114class StockKeepingUnit(
115 RootModel[
116 Annotated[
117 str,
118 Field(
119 description=(
120 'Contains a part, or a full stock keeping unit (SKU) which is used in the ordering process'
121 ' to identify the component.'
122 ),
123 min_length=1,
124 title='Stock keeping unit',
125 ),
126 ]
127 ]
128):
129 pass
132class HelperToIdentifyTheProduct(BaseModel):
133 """
134 Provides at least one method which aids in identifying the product in an asset database.
135 """
137 cpe: Annotated[
138 Optional[str],
139 Field(
140 description=(
141 'The Common Platform Enumeration (CPE) attribute refers to a method for naming platforms external'
142 ' to this specification.'
143 ),
144 min_length=5,
145 pattern=(
146 '^(cpe:2\\.3:[aho\\*\\-](:(((\\?*|\\*?)([a-zA-Z0-9\\-\\._]|'
147 '(\\\\[\\\\\\*\\?!"#\\$%&\'\\(\\)\\+,/:;<=>@\\[\\]\\^`\\{\\|\\}~]))+(\\?*|\\*?))|[\\*\\-])){5}'
148 '(:(([a-zA-Z]{2,3}(-([a-zA-Z]{2}|[0-9]{3}))?)|[\\*\\-]))(:(((\\?*|\\*?)([a-zA-Z0-9\\-\\._]|'
149 '(\\\\[\\\\\\*\\?!"#\\$%&\'\\(\\)\\+,/:;<=>@\\[\\]\\^`\\{\\|\\}~]))+(\\?*|\\*?))|[\\*\\-])){4})|'
150 '([c][pP][eE]:/[AHOaho]?(:[A-Za-z0-9\\._\\-~%]*){0,6})$'
151 ),
152 title='Common Platform Enumeration representation',
153 ),
154 ] = None
155 hashes: Annotated[
156 Optional[Sequence[CryptographicHashes]],
157 Field(
158 description='Contains a list of cryptographic hashes usable to identify files.',
159 # min_items=1,
160 title='List of hashes',
161 ),
162 ] = None
163 purl: Annotated[
164 Optional[AnyUrl],
165 Field(
166 description=(
167 'The package URL (purl) attribute refers to a method for reliably identifying and'
168 ' locating software packages external to this specification.'
169 ),
170 # min_length=7,
171 # regex='^pkg:[A-Za-z\\.\\-\\+][A-Za-z0-9\\.\\-\\+]*/.+',
172 title='package URL representation',
173 ),
174 ] = None
175 sbom_urls: Annotated[
176 Optional[Sequence[AnyUrl]],
177 Field(
178 description='Contains a list of URLs where SBOMs for this product can be retrieved.',
179 # min_items=1,
180 title='List of SBOM URLs',
181 ),
182 ] = None
183 serial_numbers: Annotated[
184 Optional[Sequence[SerialNumber]],
185 Field(
186 description='Contains a list of parts, or full serial numbers.',
187 # min_items=1,
188 title='List of serial numbers',
189 ),
190 ] = None
191 skus: Annotated[
192 Optional[Sequence[StockKeepingUnit]],
193 Field(
194 description='Contains a list of parts, or full stock keeping units.',
195 # min_items=1,
196 title='List of stock keeping units',
197 ),
198 ] = None
199 x_generic_uris: Annotated[
200 Optional[Sequence[GenericUri]],
201 Field(
202 description=(
203 'Contains a list of identifiers which are either vendor-specific or derived from'
204 ' a standard not yet supported.'
205 ),
206 # min_items=1,
207 title='List of generic URIs',
208 ),
209 ] = None
211 @classmethod
212 @no_type_check
213 @field_validator('hashes', 'sbom_urls', 'serial_numbers', 'skus', 'x_generic_uris')
214 def check_len(cls, v):
215 if not v:
216 raise ValueError('optional element present but empty')
217 return v
219 @classmethod
220 @no_type_check
221 @field_validator('purl')
222 def check_purl(cls, v):
223 if not v or len(v) < 7:
224 raise ValueError('optional purl element present but too short')
225 if not re.match('^pkg:[A-Za-z\\.\\-\\+][A-Za-z0-9\\.\\-\\+]*/.+', v):
226 raise ValueError('optional purl element present but is no purl (regex does not match)')
227 return v
230class FullProductName(BaseModel):
231 """
232 Specifies information about the product and assigns the product_id.
233 """
235 name: Annotated[
236 str,
237 Field(
238 description=(
239 "The value should be the product's full canonical name, including version number and other attributes,"
240 ' as it would be used in a human-friendly document.'
241 ),
242 examples=[
243 'Cisco AnyConnect Secure Mobility Client 2.3.185',
244 'Microsoft Host Integration Server 2006 Service Pack 1',
245 ],
246 min_length=1,
247 title='Textual description of the product',
248 ),
249 ]
250 product_id: ReferenceTokenForProductInstance
251 product_identification_helper: Annotated[
252 Optional[HelperToIdentifyTheProduct],
253 Field(
254 description='Provides at least one method which aids in identifying the product in an asset database.',
255 title='Helper to identify the product',
256 ),
257 ] = None
260class ProductGroup(BaseModel):
261 """
262 Defines a new logical group of products that can then be referred to in other parts of the document to address
263 a group of products with a single identifier.
264 """
266 group_id: ReferenceTokenForProductGroupInstance
267 product_ids: Annotated[
268 Sequence[ReferenceTokenForProductInstance],
269 Field(
270 description='Lists the product_ids of those products which known as one group in the document.',
271 # min_items=2,
272 title='List of Product IDs',
273 ),
274 ]
275 summary: Annotated[
276 Optional[str],
277 Field(
278 description='Gives a short, optional description of the group.',
279 examples=[
280 'Products supporting Modbus.',
281 'The x64 versions of the operating system.',
282 ],
283 min_length=1,
284 title='Summary of the product group',
285 ),
286 ] = None
288 @classmethod
289 @no_type_check
290 @field_validator('product_ids')
291 def check_len(cls, v):
292 if len(v) < 2:
293 raise ValueError('mandatory element present but too few items')
294 return v
297class ProductStatus(BaseModel):
298 """
299 Contains different lists of product_ids which provide details on the status of the referenced product related
300 to the current vulnerability.
301 """
303 first_affected: Annotated[
304 Optional[Products],
305 Field(
306 description='These are the first versions of the releases known to be affected by the vulnerability.',
307 title='First affected',
308 ),
309 ] = None
310 first_fixed: Annotated[
311 Optional[Products],
312 Field(
313 description='These versions contain the first fix for the vulnerability but may not be the recommended'
314 ' fixed versions.',
315 title='First fixed',
316 ),
317 ] = None
318 fixed: Annotated[
319 Optional[Products],
320 Field(
321 description='These versions contain a fix for the vulnerability but may not be the recommended'
322 ' fixed versions.',
323 title='Fixed',
324 ),
325 ] = None
326 known_affected: Annotated[
327 Optional[Products],
328 Field(
329 description='These versions are known to be affected by the vulnerability.',
330 title='Known affected',
331 ),
332 ] = None
333 known_not_affected: Annotated[
334 Optional[Products],
335 Field(
336 description='These versions are known not to be affected by the vulnerability.',
337 title='Known not affected',
338 ),
339 ] = None
340 last_affected: Annotated[
341 Optional[Products],
342 Field(
343 description='These are the last versions in a release train known to be affected by the vulnerability.'
344 ' Subsequently released versions would contain a fix for the vulnerability.',
345 title='Last affected',
346 ),
347 ] = None
348 recommended: Annotated[
349 Optional[Products],
350 Field(
351 description='These versions have a fix for the vulnerability and are the vendor-recommended versions for'
352 ' fixing the vulnerability.',
353 title='Recommended',
354 ),
355 ] = None
356 under_investigation: Annotated[
357 Optional[Products],
358 Field(
359 description='It is not known yet whether these versions are or are not affected by the vulnerability.'
360 ' However, it is still under investigation - the result will be provided in a later release'
361 ' of the document.',
362 title='Under investigation',
363 ),
364 ] = None
367class RelationshipCategory(Enum):
368 """
369 Defines the category of relationship for the referenced component.
370 """
372 default_component_of = 'default_component_of'
373 external_component_of = 'external_component_of'
374 installed_on = 'installed_on'
375 installed_with = 'installed_with'
376 optional_component_of = 'optional_component_of'
379class Relationship(BaseModel):
380 """
381 Establishes a link between two existing full_product_name_t elements, allowing the document producer to define
382 a combination of two products that form a new full_product_name entry.
383 """
385 category: Annotated[
386 RelationshipCategory,
387 Field(
388 description='Defines the category of relationship for the referenced component.',
389 title='Relationship category',
390 ),
391 ]
392 full_product_name: FullProductName
393 product_reference: Annotated[
394 ReferenceTokenForProductInstance,
395 Field(
396 description=(
397 'Holds a Product ID that refers to the Full Product Name element,'
398 ' which is referenced as the first element of the relationship.'
399 ),
400 title='Product reference',
401 ),
402 ]
403 relates_to_product_reference: Annotated[
404 ReferenceTokenForProductInstance,
405 Field(
406 description=(
407 'Holds a Product ID that refers to the Full Product Name element,'
408 ' which is referenced as the second element of the relationship.'
409 ),
410 title='Relates to product reference',
411 ),
412 ]
415class ProductTree(BaseModel):
416 """
417 Is a container for all fully qualified product names that can be referenced elsewhere in the document.
418 """
420 branches: Optional[Branches] = None
421 full_product_names: Annotated[
422 Optional[List[FullProductName]],
423 Field(
424 description='Contains a list of full product names.',
425 min_length=1,
426 title='List of full product names',
427 ),
428 ] = None
429 product_groups: Annotated[
430 Optional[List[ProductGroup]],
431 Field(
432 description='Contains a list of product groups.',
433 min_length=1,
434 title='List of product groups',
435 ),
436 ] = None
437 relationships: Annotated[
438 Optional[List[Relationship]],
439 Field(
440 description='Contains a list of relationships.',
441 min_length=1,
442 title='List of relationships',
443 ),
444 ] = None
446 @classmethod
447 @no_type_check
448 @field_validator('full_product_names', 'product_groups', 'relationships')
449 def check_len(cls, v):
450 if not v:
451 raise ValueError('optional element present but empty')
452 return v
455class BranchCategory(Enum):
456 """
457 Describes the characteristics of the labeled branch.
458 """
460 architecture = 'architecture'
461 host_name = 'host_name'
462 language = 'language'
463 legacy = 'legacy'
464 patch_level = 'patch_level'
465 product_family = 'product_family'
466 product_name = 'product_name'
467 product_version = 'product_version'
468 service_pack = 'service_pack'
469 specification = 'specification'
470 vendor = 'vendor'
473class Branch(BaseModel):
474 """
475 Is a part of the hierarchical structure of the product tree.
476 """
478 branches: Optional[Branches] = None
479 category: Annotated[
480 BranchCategory,
481 Field(
482 description='Describes the characteristics of the labeled branch.',
483 title='Category of the branch',
484 ),
485 ]
486 name: Annotated[
487 str,
488 Field(
489 description="Contains the canonical descriptor or 'friendly name' of the branch.",
490 examples=[
491 '10',
492 '365',
493 'Microsoft',
494 'Office',
495 'PCS 7',
496 'SIMATIC',
497 'Siemens',
498 'Windows',
499 ],
500 min_length=1,
501 title='Name of the branch',
502 ),
503 ]
504 product: Optional[FullProductName] = None
507class Branches(
508 RootModel[
509 Annotated[
510 List[Branch],
511 Field(
512 description='Contains branch elements as children of the current element.',
513 min_length=1,
514 title='List of branches',
515 ),
516 ]
517 ]
518):
519 """Contains branch elements as children of the current element."""
521 @classmethod
522 @no_type_check
523 @model_validator(mode='before')
524 def check_len(cls, v):
525 if not v:
526 raise ValueError('mandatory element present but empty')
527 return v
530Branch.model_rebuild()
531FullProductName.model_rebuild()
532ProductTree.model_rebuild()