Skip to content

Commit e711c87

Browse files
committed
wip to use py-serializable for output to XML
Signed-off-by: Paul Horton <[email protected]>
1 parent af94501 commit e711c87

17 files changed

+291
-232
lines changed

cyclonedx/model/__init__.py

+24-10
Original file line numberDiff line numberDiff line change
@@ -493,7 +493,8 @@ def __init__(self, *, type_: ExternalReferenceType, url: XsUri, comment: Optiona
493493
self.type_ = type_
494494
self.hashes = hashes or [] # type: ignore
495495

496-
@property
496+
@property # type: ignore[misc]
497+
@serializable.xml_sequence(1)
497498
def url(self) -> XsUri:
498499
"""
499500
The URL to the external reference.
@@ -931,7 +932,8 @@ def text(self) -> NoteText:
931932
def text(self, text: NoteText) -> None:
932933
self._text = text
933934

934-
@property
935+
@property # type: ignore[misc]
936+
@serializable.xml_sequence(1)
935937
def locale(self) -> Optional[str]:
936938
"""
937939
Get the ISO locale of this Note.
@@ -993,7 +995,8 @@ def __init__(self, *, name: Optional[str] = None, phone: Optional[str] = None, e
993995
self.email = email
994996
self.phone = phone
995997

996-
@property
998+
@property # type: ignore[misc]
999+
@serializable.xml_sequence(1)
9971000
def name(self) -> Optional[str]:
9981001
"""
9991002
Get the name of the contact.
@@ -1007,7 +1010,8 @@ def name(self) -> Optional[str]:
10071010
def name(self, name: Optional[str]) -> None:
10081011
self._name = name
10091012

1010-
@property
1013+
@property # type: ignore[misc]
1014+
@serializable.xml_sequence(2)
10111015
def email(self) -> Optional[str]:
10121016
"""
10131017
Get the email of the contact.
@@ -1021,7 +1025,8 @@ def email(self) -> Optional[str]:
10211025
def email(self, email: Optional[str]) -> None:
10221026
self._email = email
10231027

1024-
@property
1028+
@property # type: ignore[misc]
1029+
@serializable.xml_sequence(3)
10251030
def phone(self) -> Optional[str]:
10261031
"""
10271032
Get the phone of the contact.
@@ -1073,7 +1078,8 @@ def __init__(self, *, name: Optional[str] = None, urls: Optional[Iterable[XsUri]
10731078
self.urls = urls or [] # type: ignore
10741079
self.contacts = contacts or [] # type: ignore
10751080

1076-
@property
1081+
@property # type: ignore[misc]
1082+
@serializable.xml_sequence(1)
10771083
def name(self) -> Optional[str]:
10781084
"""
10791085
Get the name of the organization.
@@ -1089,6 +1095,7 @@ def name(self, name: Optional[str]) -> None:
10891095

10901096
@property # type: ignore[misc]
10911097
@serializable.xml_array(serializable.XmlArraySerializationType.FLAT, 'url')
1098+
@serializable.xml_sequence(2)
10921099
def urls(self) -> "SortedSet[XsUri]":
10931100
"""
10941101
Get a list of URLs of the organization. Multiple URLs are allowed.
@@ -1104,6 +1111,7 @@ def urls(self, urls: Iterable[XsUri]) -> None:
11041111

11051112
@property # type: ignore[misc]
11061113
@serializable.xml_array(serializable.XmlArraySerializationType.FLAT, 'contact')
1114+
@serializable.xml_sequence(3)
11071115
def contacts(self) -> "SortedSet[OrganizationalContact]":
11081116
"""
11091117
Get a list of contact person at the organization. Multiple contacts are allowed.
@@ -1154,7 +1162,8 @@ def __init__(self, *, vendor: Optional[str] = None, name: Optional[str] = None,
11541162
self.hashes = hashes or [] # type: ignore
11551163
self.external_references = external_references or [] # type: ignore
11561164

1157-
@property
1165+
@property # type: ignore[misc]
1166+
@serializable.xml_sequence(1)
11581167
def vendor(self) -> Optional[str]:
11591168
"""
11601169
The name of the vendor who created the tool.
@@ -1168,7 +1177,8 @@ def vendor(self) -> Optional[str]:
11681177
def vendor(self, vendor: Optional[str]) -> None:
11691178
self._vendor = vendor
11701179

1171-
@property
1180+
@property # type: ignore[misc]
1181+
@serializable.xml_sequence(2)
11721182
def name(self) -> Optional[str]:
11731183
"""
11741184
The name of the tool.
@@ -1182,7 +1192,8 @@ def name(self) -> Optional[str]:
11821192
def name(self, name: Optional[str]) -> None:
11831193
self._name = name
11841194

1185-
@property
1195+
@property # type: ignore[misc]
1196+
@serializable.xml_sequence(3)
11861197
def version(self) -> Optional[str]:
11871198
"""
11881199
The version of the tool.
@@ -1196,7 +1207,9 @@ def version(self) -> Optional[str]:
11961207
def version(self, version: Optional[str]) -> None:
11971208
self._version = version
11981209

1199-
@property
1210+
@property # type: ignore[misc]
1211+
@serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'hash')
1212+
@serializable.xml_sequence(4)
12001213
def hashes(self) -> "SortedSet[HashType]":
12011214
"""
12021215
The hashes of the tool (if applicable).
@@ -1212,6 +1225,7 @@ def hashes(self, hashes: Iterable[HashType]) -> None:
12121225

12131226
@property # type: ignore[misc]
12141227
@serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'reference')
1228+
@serializable.xml_sequence(5)
12151229
def external_references(self) -> "SortedSet[ExternalReference]":
12161230
"""
12171231
External References provide a way to document systems, sites, and information that may be relevant but which

cyclonedx/model/bom.py

+58-8
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,15 @@
2222
from uuid import UUID, uuid4
2323

2424
import serializable
25+
from cyclonedx.serialization import UrnUuidHelper
2526
from sortedcontainers import SortedSet
2627

2728
from ..exception.model import UnknownComponentDependencyException
2829
from ..parser import BaseParser
2930
from . import ExternalReference, LicenseChoice, OrganizationalContact, OrganizationalEntity, Property, ThisTool, Tool
3031
from .bom_ref import BomRef
3132
from .component import Component
33+
from .dependency import Dependable, Dependency
3234
from .service import Service
3335
from .vulnerability import Vulnerability
3436

@@ -63,6 +65,7 @@ def __init__(self, *, tools: Optional[Iterable[Tool]] = None,
6365

6466
@property # type: ignore[misc]
6567
@serializable.type_mapping(serializable.helpers.XsdDateTime)
68+
@serializable.xml_sequence(1)
6669
def timestamp(self) -> datetime:
6770
"""
6871
The date and time (in UTC) when this BomMetaData was created.
@@ -78,6 +81,7 @@ def timestamp(self, timestamp: datetime) -> None:
7881

7982
@property # type: ignore[misc]
8083
@serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'tool')
84+
@serializable.xml_sequence(2)
8185
def tools(self) -> "SortedSet[Tool]":
8286
"""
8387
Tools used to create this BOM.
@@ -93,6 +97,7 @@ def tools(self, tools: Iterable[Tool]) -> None:
9397

9498
@property # type: ignore[misc]
9599
@serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'author')
100+
@serializable.xml_sequence(3)
96101
def authors(self) -> "SortedSet[OrganizationalContact]":
97102
"""
98103
The person(s) who created the BOM.
@@ -110,7 +115,8 @@ def authors(self) -> "SortedSet[OrganizationalContact]":
110115
def authors(self, authors: Iterable[OrganizationalContact]) -> None:
111116
self._authors = SortedSet(authors)
112117

113-
@property
118+
@property # type: ignore[misc]
119+
@serializable.xml_sequence(4)
114120
def component(self) -> Optional[Component]:
115121
"""
116122
The (optional) component that the BOM describes.
@@ -134,7 +140,8 @@ def component(self, component: Component) -> None:
134140
"""
135141
self._component = component
136142

137-
@property
143+
@property # type: ignore[misc]
144+
@serializable.xml_sequence(5)
138145
def manufacture(self) -> Optional[OrganizationalEntity]:
139146
"""
140147
The organization that manufactured the component that the BOM describes.
@@ -148,7 +155,8 @@ def manufacture(self) -> Optional[OrganizationalEntity]:
148155
def manufacture(self, manufacture: Optional[OrganizationalEntity]) -> None:
149156
self._manufacture = manufacture
150157

151-
@property
158+
@property # type: ignore[misc]
159+
@serializable.xml_sequence(6)
152160
def supplier(self) -> Optional[OrganizationalEntity]:
153161
"""
154162
The organization that supplied the component that the BOM describes.
@@ -166,6 +174,7 @@ def supplier(self, supplier: Optional[OrganizationalEntity]) -> None:
166174

167175
@property # type: ignore[misc]
168176
@serializable.xml_array(serializable.XmlArraySerializationType.FLAT, '')
177+
@serializable.xml_sequence(7)
169178
def licenses(self) -> "SortedSet[LicenseChoice]":
170179
"""
171180
A optional list of statements about how this BOM is licensed.
@@ -181,6 +190,7 @@ def licenses(self, licenses: Iterable[LicenseChoice]) -> None:
181190

182191
@property # type: ignore[misc]
183192
@serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'property')
193+
@serializable.xml_sequence(8)
184194
def properties(self) -> "SortedSet[Property]":
185195
"""
186196
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,
248258
services: Optional[Iterable[Service]] = None,
249259
external_references: Optional[Iterable[ExternalReference]] = None,
250260
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:
252263
"""
253264
Create a new Bom that you can manually/programmatically add data to later.
254265
@@ -262,22 +273,27 @@ def __init__(self, *, components: Optional[Iterable[Component]] = None,
262273
self.external_references = external_references or [] # type: ignore
263274
self.vulnerabilities = SortedSet()
264275
self.version = version
276+
self.dependencies = dependencies or [] # type: ignore
265277

266-
@property
278+
@property # type: ignore[misc]
279+
@serializable.type_mapping(UrnUuidHelper)
280+
@serializable.xml_attribute()
267281
def serial_number(self) -> UUID:
268282
"""
269283
Unique UUID for this BOM
270284
271285
Returns:
272286
`UUID` instance
287+
`UUID` instance
273288
"""
274289
return self._serial_number
275290

276291
@serial_number.setter
277292
def serial_number(self, serial_number: UUID) -> None:
278293
self._serial_number = serial_number
279294

280-
@property
295+
@property # type: ignore[misc]
296+
@serializable.xml_sequence(1)
281297
def metadata(self) -> BomMetaData:
282298
"""
283299
Get our internal metadata object for this Bom.
@@ -296,6 +312,7 @@ def metadata(self, metadata: BomMetaData) -> None:
296312

297313
@property # type: ignore[misc]
298314
@serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'component')
315+
@serializable.xml_sequence(2)
299316
def components(self) -> "SortedSet[Component]":
300317
"""
301318
Get all the Components currently in this Bom.
@@ -351,6 +368,7 @@ def has_component(self, component: Component) -> bool:
351368

352369
@property # type: ignore[misc]
353370
@serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'service')
371+
@serializable.xml_sequence(3)
354372
def services(self) -> "SortedSet[Service]":
355373
"""
356374
Get all the Services currently in this Bom.
@@ -366,6 +384,7 @@ def services(self, services: Iterable[Service]) -> None:
366384

367385
@property # type: ignore[misc]
368386
@serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'reference')
387+
@serializable.xml_sequence(4)
369388
def external_references(self) -> "SortedSet[ExternalReference]":
370389
"""
371390
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:
418437

419438
@property # type: ignore[misc]
420439
@serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'vulnerability')
440+
@serializable.xml_sequence(8)
421441
def vulnerabilities(self) -> "SortedSet[Vulnerability]":
422442
"""
423443
Get all the Vulnerabilities in this BOM.
@@ -432,13 +452,36 @@ def vulnerabilities(self, vulnerabilities: Iterable[Vulnerability]) -> None:
432452
self._vulnerabilities = SortedSet(vulnerabilities)
433453

434454
@property
455+
@serializable.xml_attribute()
435456
def version(self) -> int:
436457
return self._version
437458

438459
@version.setter
439460
def version(self, version: int) -> None:
440461
self._version = version
441462

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+
442485
def urn(self) -> str:
443486
return f'urn:cdx:{self.serial_number}/{self.version}'
444487

@@ -450,20 +493,27 @@ def validate(self) -> bool:
450493
Returns:
451494
`bool`
452495
"""
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)
453503

454504
# 1. Make sure dependencies are all in this Bom.
455505
all_bom_refs = set(map(lambda c: c.bom_ref, self._get_all_components())) | set(
456506
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))
457508

458-
all_dependency_bom_refs = set().union(*(c.dependencies for c in self.components))
459509
dependency_diff = all_dependency_bom_refs - all_bom_refs
460510
if len(dependency_diff) > 0:
461511
raise UnknownComponentDependencyException(
462512
f'One or more Components have Dependency references to Components/Services that are not known in this '
463513
f'BOM. They are: {dependency_diff}')
464514

465515
# 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):
467517
warnings.warn(
468518
f'The Component this BOM is describing {self.metadata.component.purl} has no defined dependencies '
469519
f'which means the Dependency Graph is incomplete - you should add direct dependencies to this Component'

0 commit comments

Comments
 (0)