22
22
from uuid import UUID , uuid4
23
23
24
24
import serializable
25
+ from cyclonedx .serialization import UrnUuidHelper
25
26
from sortedcontainers import SortedSet
26
27
27
28
from ..exception .model import UnknownComponentDependencyException
28
29
from ..parser import BaseParser
29
30
from . import ExternalReference , LicenseChoice , OrganizationalContact , OrganizationalEntity , Property , ThisTool , Tool
30
31
from .bom_ref import BomRef
31
32
from .component import Component
33
+ from .dependency import Dependable , Dependency
32
34
from .service import Service
33
35
from .vulnerability import Vulnerability
34
36
@@ -63,6 +65,7 @@ def __init__(self, *, tools: Optional[Iterable[Tool]] = None,
63
65
64
66
@property # type: ignore[misc]
65
67
@serializable .type_mapping (serializable .helpers .XsdDateTime )
68
+ @serializable .xml_sequence (1 )
66
69
def timestamp (self ) -> datetime :
67
70
"""
68
71
The date and time (in UTC) when this BomMetaData was created.
@@ -78,6 +81,7 @@ def timestamp(self, timestamp: datetime) -> None:
78
81
79
82
@property # type: ignore[misc]
80
83
@serializable .xml_array (serializable .XmlArraySerializationType .NESTED , 'tool' )
84
+ @serializable .xml_sequence (2 )
81
85
def tools (self ) -> "SortedSet[Tool]" :
82
86
"""
83
87
Tools used to create this BOM.
@@ -93,6 +97,7 @@ def tools(self, tools: Iterable[Tool]) -> None:
93
97
94
98
@property # type: ignore[misc]
95
99
@serializable .xml_array (serializable .XmlArraySerializationType .NESTED , 'author' )
100
+ @serializable .xml_sequence (3 )
96
101
def authors (self ) -> "SortedSet[OrganizationalContact]" :
97
102
"""
98
103
The person(s) who created the BOM.
@@ -110,7 +115,8 @@ def authors(self) -> "SortedSet[OrganizationalContact]":
110
115
def authors (self , authors : Iterable [OrganizationalContact ]) -> None :
111
116
self ._authors = SortedSet (authors )
112
117
113
- @property
118
+ @property # type: ignore[misc]
119
+ @serializable .xml_sequence (4 )
114
120
def component (self ) -> Optional [Component ]:
115
121
"""
116
122
The (optional) component that the BOM describes.
@@ -134,7 +140,8 @@ def component(self, component: Component) -> None:
134
140
"""
135
141
self ._component = component
136
142
137
- @property
143
+ @property # type: ignore[misc]
144
+ @serializable .xml_sequence (5 )
138
145
def manufacture (self ) -> Optional [OrganizationalEntity ]:
139
146
"""
140
147
The organization that manufactured the component that the BOM describes.
@@ -148,7 +155,8 @@ def manufacture(self) -> Optional[OrganizationalEntity]:
148
155
def manufacture (self , manufacture : Optional [OrganizationalEntity ]) -> None :
149
156
self ._manufacture = manufacture
150
157
151
- @property
158
+ @property # type: ignore[misc]
159
+ @serializable .xml_sequence (6 )
152
160
def supplier (self ) -> Optional [OrganizationalEntity ]:
153
161
"""
154
162
The organization that supplied the component that the BOM describes.
@@ -166,6 +174,7 @@ def supplier(self, supplier: Optional[OrganizationalEntity]) -> None:
166
174
167
175
@property # type: ignore[misc]
168
176
@serializable .xml_array (serializable .XmlArraySerializationType .FLAT , '' )
177
+ @serializable .xml_sequence (7 )
169
178
def licenses (self ) -> "SortedSet[LicenseChoice]" :
170
179
"""
171
180
A optional list of statements about how this BOM is licensed.
@@ -181,6 +190,7 @@ def licenses(self, licenses: Iterable[LicenseChoice]) -> None:
181
190
182
191
@property # type: ignore[misc]
183
192
@serializable .xml_array (serializable .XmlArraySerializationType .NESTED , 'property' )
193
+ @serializable .xml_sequence (8 )
184
194
def properties (self ) -> "SortedSet[Property]" :
185
195
"""
186
196
Provides the ability to document properties in a key/value store. This provides flexibility to include data not
@@ -248,7 +258,8 @@ def __init__(self, *, components: Optional[Iterable[Component]] = None,
248
258
services : Optional [Iterable [Service ]] = None ,
249
259
external_references : Optional [Iterable [ExternalReference ]] = None ,
250
260
serial_number : Optional [UUID ] = None , version : int = 1 ,
251
- metadata : Optional [BomMetaData ] = None ) -> None :
261
+ metadata : Optional [BomMetaData ] = None ,
262
+ dependencies : Optional [Iterable [Dependency ]] = None ) -> None :
252
263
"""
253
264
Create a new Bom that you can manually/programmatically add data to later.
254
265
@@ -262,22 +273,27 @@ def __init__(self, *, components: Optional[Iterable[Component]] = None,
262
273
self .external_references = external_references or [] # type: ignore
263
274
self .vulnerabilities = SortedSet ()
264
275
self .version = version
276
+ self .dependencies = dependencies or [] # type: ignore
265
277
266
- @property
278
+ @property # type: ignore[misc]
279
+ @serializable .type_mapping (UrnUuidHelper )
280
+ @serializable .xml_attribute ()
267
281
def serial_number (self ) -> UUID :
268
282
"""
269
283
Unique UUID for this BOM
270
284
271
285
Returns:
272
286
`UUID` instance
287
+ `UUID` instance
273
288
"""
274
289
return self ._serial_number
275
290
276
291
@serial_number .setter
277
292
def serial_number (self , serial_number : UUID ) -> None :
278
293
self ._serial_number = serial_number
279
294
280
- @property
295
+ @property # type: ignore[misc]
296
+ @serializable .xml_sequence (1 )
281
297
def metadata (self ) -> BomMetaData :
282
298
"""
283
299
Get our internal metadata object for this Bom.
@@ -296,6 +312,7 @@ def metadata(self, metadata: BomMetaData) -> None:
296
312
297
313
@property # type: ignore[misc]
298
314
@serializable .xml_array (serializable .XmlArraySerializationType .NESTED , 'component' )
315
+ @serializable .xml_sequence (2 )
299
316
def components (self ) -> "SortedSet[Component]" :
300
317
"""
301
318
Get all the Components currently in this Bom.
@@ -351,6 +368,7 @@ def has_component(self, component: Component) -> bool:
351
368
352
369
@property # type: ignore[misc]
353
370
@serializable .xml_array (serializable .XmlArraySerializationType .NESTED , 'service' )
371
+ @serializable .xml_sequence (3 )
354
372
def services (self ) -> "SortedSet[Service]" :
355
373
"""
356
374
Get all the Services currently in this Bom.
@@ -366,6 +384,7 @@ def services(self, services: Iterable[Service]) -> None:
366
384
367
385
@property # type: ignore[misc]
368
386
@serializable .xml_array (serializable .XmlArraySerializationType .NESTED , 'reference' )
387
+ @serializable .xml_sequence (4 )
369
388
def external_references (self ) -> "SortedSet[ExternalReference]" :
370
389
"""
371
390
Provides the ability to document external references related to the BOM or to the project the BOM describes.
@@ -418,6 +437,7 @@ def has_vulnerabilities(self) -> bool:
418
437
419
438
@property # type: ignore[misc]
420
439
@serializable .xml_array (serializable .XmlArraySerializationType .NESTED , 'vulnerability' )
440
+ @serializable .xml_sequence (8 )
421
441
def vulnerabilities (self ) -> "SortedSet[Vulnerability]" :
422
442
"""
423
443
Get all the Vulnerabilities in this BOM.
@@ -432,13 +452,36 @@ def vulnerabilities(self, vulnerabilities: Iterable[Vulnerability]) -> None:
432
452
self ._vulnerabilities = SortedSet (vulnerabilities )
433
453
434
454
@property
455
+ @serializable .xml_attribute ()
435
456
def version (self ) -> int :
436
457
return self ._version
437
458
438
459
@version .setter
439
460
def version (self , version : int ) -> None :
440
461
self ._version = version
441
462
463
+ @property
464
+ @serializable .xml_array (serializable .XmlArraySerializationType .NESTED , 'dependency' )
465
+ @serializable .xml_sequence (5 )
466
+ def dependencies (self ) -> "SortedSet[Dependency]" :
467
+ return self ._dependencies
468
+
469
+ @dependencies .setter
470
+ def dependencies (self , dependencies : Iterable [Dependency ]) -> None :
471
+ self ._dependencies = SortedSet (dependencies )
472
+
473
+ def register_dependency (self , target : Dependable , depends_on : Optional [Iterable [Dependable ]] = None ) -> None :
474
+ _d = next (filter (lambda _d : _d .ref == target .bom_ref , self .dependencies ), None )
475
+
476
+ if _d and depends_on :
477
+ _d .dependencies = _d .dependencies + set (
478
+ map (lambda _d : Dependency (ref = _d ), depends_on )) if depends_on else []
479
+ else :
480
+ self ._dependencies .add (Dependency (
481
+ ref = target .bom_ref ,
482
+ dependencies = list (map (lambda _d : Dependency (ref = _d ), depends_on )) if depends_on else []
483
+ ))
484
+
442
485
def urn (self ) -> str :
443
486
return f'urn:cdx:{ self .serial_number } /{ self .version } '
444
487
@@ -450,20 +493,27 @@ def validate(self) -> bool:
450
493
Returns:
451
494
`bool`
452
495
"""
496
+ # 0. Make sure all Dependable have a Dependency entry
497
+ if self .metadata .component :
498
+ self .register_dependency (target = self .metadata .component )
499
+ for _c in self .components :
500
+ self .register_dependency (target = _c )
501
+ for _s in self .services :
502
+ self .register_dependency (target = _s )
453
503
454
504
# 1. Make sure dependencies are all in this Bom.
455
505
all_bom_refs = set (map (lambda c : c .bom_ref , self ._get_all_components ())) | set (
456
506
map (lambda s : s .bom_ref , self .services ))
507
+ all_dependency_bom_refs = set ().union (* (d .dependencies_as_bom_refs () for d in self .dependencies ))
457
508
458
- all_dependency_bom_refs = set ().union (* (c .dependencies for c in self .components ))
459
509
dependency_diff = all_dependency_bom_refs - all_bom_refs
460
510
if len (dependency_diff ) > 0 :
461
511
raise UnknownComponentDependencyException (
462
512
f'One or more Components have Dependency references to Components/Services that are not known in this '
463
513
f'BOM. They are: { dependency_diff } ' )
464
514
465
515
# 2. Dependencies should exist for the Component this BOM is describing, if one is set
466
- if self .metadata .component and not self .metadata .component .dependencies :
516
+ if self .metadata .component and filter ( lambda _d : _d . ref == self .metadata .component .bom_ref , self . dependencies ) :
467
517
warnings .warn (
468
518
f'The Component this BOM is describing { self .metadata .component .purl } has no defined dependencies '
469
519
f'which means the Dependency Graph is incomplete - you should add direct dependencies to this Component'
0 commit comments