Skip to content

Commit 1b733d7

Browse files
committed
feat: support for bom.externalReferences in JSON and XML #124
Signed-off-by: Paul Horton <[email protected]>
1 parent 32c0139 commit 1b733d7

15 files changed

+372
-16
lines changed

cyclonedx/model/bom.py

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
from typing import cast, List, Optional
2222
from uuid import uuid4, UUID
2323

24-
from . import ThisTool, Tool
24+
from . import ExternalReference, ThisTool, Tool
2525
from .component import Component
2626
from .service import Service
2727
from ..parser import BaseParser
@@ -149,7 +149,8 @@ def from_parser(parser: BaseParser) -> 'Bom':
149149
bom.add_components(parser.get_components())
150150
return bom
151151

152-
def __init__(self, components: Optional[List[Component]] = None, services: Optional[List[Service]] = None) -> None:
152+
def __init__(self, components: Optional[List[Component]] = None, services: Optional[List[Service]] = None,
153+
external_references: Optional[List[ExternalReference]] = None) -> None:
153154
"""
154155
Create a new Bom that you can manually/programmatically add data to later.
155156
@@ -160,6 +161,7 @@ def __init__(self, components: Optional[List[Component]] = None, services: Optio
160161
self.metadata = BomMetaData()
161162
self.components = components
162163
self.services = services
164+
self.external_references = external_references
163165

164166
@property
165167
def uuid(self) -> UUID:
@@ -360,6 +362,33 @@ def service_count(self) -> int:
360362

361363
return len(self.services)
362364

365+
@property
366+
def external_references(self) -> Optional[List[ExternalReference]]:
367+
"""
368+
Provides the ability to document external references related to the BOM or to the project the BOM describes.
369+
370+
Returns:
371+
List of `ExternalReference` else `None`
372+
"""
373+
return self._external_references
374+
375+
@external_references.setter
376+
def external_references(self, external_references: Optional[List[ExternalReference]]) -> None:
377+
self._external_references = external_references
378+
379+
def add_external_reference(self, external_reference: ExternalReference) -> None:
380+
"""
381+
Add an external reference to this Bom.
382+
383+
Args:
384+
external_reference:
385+
`ExternalReference` to add to this Bom.
386+
387+
Returns:
388+
None
389+
"""
390+
self.external_references = (self.external_references or []) + [external_reference]
391+
363392
def has_vulnerabilities(self) -> bool:
364393
"""
365394
Check whether this Bom has any declared vulnerabilities.

cyclonedx/output/json.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,13 @@ def _specialise_output_for_schema_version(self, bom_json: Dict[Any, Any]) -> str
113113
if not self.services_supports_release_notes() and 'releaseNotes' in bom_json['services'][i].keys():
114114
del bom_json['services'][i]['releaseNotes']
115115

116+
# Iterate externalReferences
117+
if 'externalReferences' in bom_json.keys():
118+
for i in range(len(bom_json['externalReferences'])):
119+
if not self.external_references_supports_hashes() \
120+
and 'hashes' in bom_json['externalReferences'][i].keys():
121+
del bom_json['externalReferences'][i]['hashes']
122+
116123
# Iterate Vulnerabilities
117124
if 'vulnerabilities' in bom_json.keys():
118125
for i in range(len(bom_json['vulnerabilities'])):

cyclonedx/output/schema.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ def bom_metadata_supports_tools_external_references(self) -> bool:
4141
def bom_supports_services(self) -> bool:
4242
return True
4343

44+
def bom_supports_external_references(self) -> bool:
45+
return True
46+
4447
def services_supports_properties(self) -> bool:
4548
return True
4649

@@ -236,6 +239,9 @@ def bom_metadata_supports_tools_external_references(self) -> bool:
236239
def bom_supports_services(self) -> bool:
237240
return False
238241

242+
def bom_supports_external_references(self) -> bool:
243+
return False
244+
239245
def services_supports_properties(self) -> bool:
240246
return False
241247

cyclonedx/output/xml.py

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,13 @@ def generate(self, force_regeneration: bool = False) -> None:
8484
for service in cast(List[Service], self.get_bom().services):
8585
services_element.append(self._add_service_element(service=service))
8686

87+
if self.bom_supports_external_references():
88+
if self.get_bom().external_references:
89+
self._add_external_references_to_element(
90+
ext_refs=cast(List[ExternalReference], self.get_bom().external_references),
91+
element=self._root_bom_element
92+
)
93+
8794
if self.bom_supports_vulnerabilities() and has_vulnerabilities:
8895
vulnerabilities_element = ElementTree.SubElement(self._root_bom_element, 'vulnerabilities')
8996
for component in cast(List[Component], self.get_bom().components):
@@ -276,18 +283,7 @@ def _add_component_element(self, component: Component) -> ElementTree.Element:
276283

277284
# externalReferences
278285
if self.component_supports_external_references() and len(component.external_references) > 0:
279-
external_references_e = ElementTree.SubElement(component_element, 'externalReferences')
280-
for ext_ref in component.external_references:
281-
external_reference_e = ElementTree.SubElement(
282-
external_references_e, 'reference', {'type': ext_ref.get_reference_type().value}
283-
)
284-
ElementTree.SubElement(external_reference_e, 'url').text = ext_ref.get_url()
285-
286-
if ext_ref.get_comment():
287-
ElementTree.SubElement(external_reference_e, 'comment').text = ext_ref.get_comment()
288-
289-
if self.external_references_supports_hashes() and len(ext_ref.get_hashes()) > 0:
290-
Xml._add_hashes_to_element(hashes=ext_ref.get_hashes(), element=external_reference_e)
286+
self._add_external_references_to_element(ext_refs=component.external_references, element=component_element)
291287

292288
# releaseNotes
293289
if self.component_supports_release_notes() and component.release_notes:

tests/data.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,13 @@ def get_bom_just_complete_metadata() -> Bom:
154154
return bom
155155

156156

157+
def get_bom_with_external_references() -> Bom:
158+
bom = Bom(external_references=[
159+
get_external_reference_1(), get_external_reference_2()
160+
])
161+
return bom
162+
163+
157164
def get_bom_with_services_simple() -> Bom:
158165
bom = Bom(services=[
159166
Service(name='my-first-service'),
@@ -288,6 +295,13 @@ def get_external_reference_1() -> ExternalReference:
288295
)
289296

290297

298+
def get_external_reference_2() -> ExternalReference:
299+
return ExternalReference(
300+
reference_type=ExternalReferenceType.WEBSITE,
301+
url='https://cyclonedx.org'
302+
)
303+
304+
291305
def get_issue_1() -> IssueType:
292306
return IssueType(
293307
classification=IssueClassification.SECURITY, id='CVE-2021-44228', name='Apache Log3Shell',
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{
2+
"$schema": "http://cyclonedx.org/schema/bom-1.2a.schema.json",
3+
"bomFormat": "CycloneDX",
4+
"specVersion": "1.2",
5+
"serialNumber": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79",
6+
"version": 1,
7+
"metadata": {
8+
"timestamp": "2021-09-01T10:50:42.051979+00:00",
9+
"tools": [
10+
{
11+
"vendor": "CycloneDX",
12+
"name": "cyclonedx-python-lib",
13+
"version": "VERSION"
14+
}
15+
]
16+
},
17+
"components": [],
18+
"externalReferences": [
19+
{
20+
"url": "https://cyclonedx.org",
21+
"comment": "No comment",
22+
"type": "distribution"
23+
},
24+
{
25+
"url": "https://cyclonedx.org",
26+
"type": "website"
27+
}
28+
]
29+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
{
2+
"$schema": "http://cyclonedx.org/schema/bom-1.3.schema.json",
3+
"bomFormat": "CycloneDX",
4+
"specVersion": "1.3",
5+
"serialNumber": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79",
6+
"version": 1,
7+
"metadata": {
8+
"timestamp": "2021-09-01T10:50:42.051979+00:00",
9+
"tools": [
10+
{
11+
"vendor": "CycloneDX",
12+
"name": "cyclonedx-python-lib",
13+
"version": "VERSION"
14+
}
15+
]
16+
},
17+
"components": [],
18+
"externalReferences": [
19+
{
20+
"url": "https://cyclonedx.org",
21+
"comment": "No comment",
22+
"type": "distribution",
23+
"hashes": [
24+
{
25+
"alg": "SHA-256",
26+
"content": "806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"
27+
}
28+
]
29+
},
30+
{
31+
"url": "https://cyclonedx.org",
32+
"type": "website"
33+
}
34+
]
35+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
{
2+
"$schema": "http://cyclonedx.org/schema/bom-1.4.schema.json",
3+
"bomFormat": "CycloneDX",
4+
"specVersion": "1.4",
5+
"serialNumber": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79",
6+
"version": 1,
7+
"metadata": {
8+
"timestamp": "2021-09-01T10:50:42.051979+00:00",
9+
"tools": [
10+
{
11+
"vendor": "CycloneDX",
12+
"name": "cyclonedx-python-lib",
13+
"version": "VERSION",
14+
"externalReferences": [
15+
{
16+
"type": "build-system",
17+
"url": "https://github.com/CycloneDX/cyclonedx-python-lib/actions"
18+
},
19+
{
20+
"type": "distribution",
21+
"url": "https://pypi.org/project/cyclonedx-python-lib/"
22+
},
23+
{
24+
"type": "documentation",
25+
"url": "https://cyclonedx.github.io/cyclonedx-python-lib/"
26+
},
27+
{
28+
"type": "issue-tracker",
29+
"url": "https://github.com/CycloneDX/cyclonedx-python-lib/issues"
30+
},
31+
{
32+
"type": "license",
33+
"url": "https://github.com/CycloneDX/cyclonedx-python-lib/blob/main/LICENSE"
34+
},
35+
{
36+
"type": "release-notes",
37+
"url": "https://github.com/CycloneDX/cyclonedx-python-lib/blob/main/CHANGELOG.md"
38+
},
39+
{
40+
"type": "vcs",
41+
"url": "https://github.com/CycloneDX/cyclonedx-python-lib"
42+
},
43+
{
44+
"type": "website",
45+
"url": "https://cyclonedx.org"
46+
}
47+
]
48+
}
49+
]
50+
},
51+
"components": [],
52+
"externalReferences": [
53+
{
54+
"url": "https://cyclonedx.org",
55+
"comment": "No comment",
56+
"type": "distribution",
57+
"hashes": [
58+
{
59+
"alg": "SHA-256",
60+
"content": "806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"
61+
}
62+
]
63+
},
64+
{
65+
"url": "https://cyclonedx.org",
66+
"type": "website"
67+
}
68+
]
69+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<bom xmlns="http://cyclonedx.org/schema/bom/1.1" version="1">
3+
<components />
4+
<externalReferences>
5+
<reference type="distribution">
6+
<url>https://cyclonedx.org</url>
7+
<comment>No comment</comment>
8+
</reference>
9+
<reference type="website">
10+
<url>https://cyclonedx.org</url>
11+
</reference>
12+
</externalReferences>
13+
</bom>
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<bom xmlns="http://cyclonedx.org/schema/bom/1.2" version="1">
3+
<metadata>
4+
<timestamp>2021-09-01T10:50:42.051979+00:00</timestamp>
5+
<tools>
6+
<tool>
7+
<vendor>CycloneDX</vendor>
8+
<name>cyclonedx-python-lib</name>
9+
<version>VERSION</version>
10+
</tool>
11+
</tools>
12+
</metadata>
13+
<components />
14+
<externalReferences>
15+
<reference type="distribution">
16+
<url>https://cyclonedx.org</url>
17+
<comment>No comment</comment>
18+
</reference>
19+
<reference type="website">
20+
<url>https://cyclonedx.org</url>
21+
</reference>
22+
</externalReferences>
23+
</bom>

0 commit comments

Comments
 (0)