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
« prev ^ index » next coverage.py v7.4.1, created at 2024-02-05 19:27:17 +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 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 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
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
137class HelperToIdentifyTheProduct(BaseModel):
138 """
139 Provides at least one method which aids in identifying the product in an asset database.
140 """
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
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
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
235class FullProductName(BaseModel):
236 """
237 Specifies information about the product and assigns the product_id.
238 """
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
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 """
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
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
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 """
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
372class RelationshipCategory(Enum):
373 """
374 Defines the category of relationship for the referenced component.
375 """
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'
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 """
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 ]
420class ProductTree(BaseModel):
421 """
422 Is a container for all fully qualified product names that can be referenced elsewhere in the document.
423 """
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
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
460class BranchCategory(Enum):
461 """
462 Describes the characteristics of the labeled branch.
463 """
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'
478class Branch(BaseModel):
479 """
480 Is a part of the hierarchical structure of the product tree.
481 """
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
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."""
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
535Branch.model_rebuild()
536FullProductName.model_rebuild()
537ProductTree.model_rebuild()