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
« prev ^ index » next coverage.py v7.6.9, created at 2024-12-18 20:29:38 +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 turvallisuusneuvonta.csaf.definitions import (
13 AnyUrl,
14 Products,
15 ReferenceTokenForProductGroupInstance,
16 ReferenceTokenForProductInstance,
17)
20class FileHash(BaseModel):
21 """
22 Contains one hash value and algorithm of the file to be identified.
23 """
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 ]
53class CryptographicHashes(BaseModel):
54 """
55 Contains all information to identify a file based on its cryptographic hash values.
56 """
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 ]
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
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 """
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')]
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
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
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
152class HelperToIdentifyTheProduct(BaseModel):
153 """
154 Provides at least one method which aids in identifying the product in an asset database.
155 """
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
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
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
261class FullProductName(BaseModel):
262 """
263 Specifies information about the product and assigns the product_id.
264 """
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
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 """
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
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
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 """
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
398class RelationshipCategory(Enum):
399 """
400 Defines the category of relationship for the referenced component.
401 """
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'
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 """
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 ]
446class ProductTree(BaseModel):
447 """
448 Is a container for all fully qualified product names that can be referenced elsewhere in the document.
449 """
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
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
486class BranchCategory(Enum):
487 """
488 Describes the characteristics of the labeled branch.
489 """
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'
505class Branch(BaseModel):
506 """
507 Is a part of the hierarchical structure of the product tree.
508 """
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
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."""
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
562Branch.model_rebuild()
563FullProductName.model_rebuild()
564ProductTree.model_rebuild()