Skip to content

Commit 1ac31f4

Browse files
feat: add support for bom.metadata.component (#118)
* Add support for metadata component Part of #6 Signed-off-by: Artem Smotrakov <[email protected]> * Better docs and simpler ifs Signed-off-by: Artem Smotrakov <[email protected]>
1 parent 3509fb6 commit 1ac31f4

File tree

9 files changed

+124
-9
lines changed

9 files changed

+124
-9
lines changed

cyclonedx/model/bom.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ def __init__(self, tools: Optional[List[Tool]] = None) -> None:
4141
if not self.tools:
4242
self.add_tool(ThisTool)
4343

44+
self.component: Optional[Component] = None
45+
4446
@property
4547
def tools(self) -> List[Tool]:
4648
"""
@@ -80,6 +82,30 @@ def timestamp(self) -> datetime:
8082
def timestamp(self, timestamp: datetime) -> None:
8183
self._timestamp = timestamp
8284

85+
@property
86+
def component(self) -> Optional[Component]:
87+
"""
88+
The (optional) component that the BOM describes.
89+
90+
Returns:
91+
`cyclonedx.model.component.Component` instance for this Bom Metadata.
92+
"""
93+
return self._component
94+
95+
@component.setter
96+
def component(self, component: Component) -> None:
97+
"""
98+
The (optional) component that the BOM describes.
99+
100+
Args:
101+
component
102+
`cyclonedx.model.component.Component` instance to add to this Bom Metadata.
103+
104+
Returns:
105+
None
106+
"""
107+
self._component = component
108+
83109

84110
class Bom:
85111
"""

cyclonedx/output/json.py

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,13 @@
2828
from ..model.bom import Bom
2929

3030

31+
ComponentDict = Dict[str, Union[
32+
str,
33+
List[Dict[str, str]],
34+
List[Dict[str, Dict[str, str]]],
35+
List[Dict[str, Union[str, List[Dict[str, str]]]]]]]
36+
37+
3138
class Json(BaseOutput, BaseSchemaVersion):
3239

3340
def __init__(self, bom: Bom) -> None:
@@ -73,15 +80,19 @@ def _specialise_output_for_schema_version(self, bom_json: Dict[Any, Any]) -> str
7380
del bom_json['metadata']['tools'][i]['externalReferences']
7481

7582
# Iterate Components
76-
for i in range(len(bom_json['components'])):
77-
if not self.component_supports_author() and 'author' in bom_json['components'][i].keys():
78-
del bom_json['components'][i]['author']
83+
if 'components' in bom_json.keys():
84+
for i in range(len(bom_json['components'])):
85+
if not self.component_supports_author() and 'author' in bom_json['components'][i].keys():
86+
del bom_json['components'][i]['author']
7987

80-
if not self.component_supports_mime_type_attribute() and 'mime-type' in bom_json['components'][i].keys():
81-
del bom_json['components'][i]['mime-type']
88+
if not self.component_supports_mime_type_attribute() \
89+
and 'mime-type' in bom_json['components'][i].keys():
90+
del bom_json['components'][i]['mime-type']
8291

83-
if not self.component_supports_release_notes() and 'releaseNotes' in bom_json['components'][i].keys():
84-
del bom_json['components'][i]['releaseNotes']
92+
if not self.component_supports_release_notes() and 'releaseNotes' in bom_json['components'][i].keys():
93+
del bom_json['components'][i]['releaseNotes']
94+
else:
95+
bom_json['components'] = []
8596

8697
# Iterate Vulnerabilities
8798
if 'vulnerabilities' in bom_json.keys():

cyclonedx/output/xml.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,9 @@ def _add_metadata_element(self) -> None:
113113
for tool in bom_metadata.tools:
114114
self._add_tool(parent_element=tools_e, tool=tool)
115115

116+
if bom_metadata.component:
117+
metadata_e.append(self._add_component_element(component=bom_metadata.component))
118+
116119
def _add_component_element(self, component: Component) -> ElementTree.Element:
117120
element_attributes = {'type': component.type.value}
118121
if self.component_supports_bom_ref_attribute() and component.bom_ref:
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
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+
"component": {
17+
"type": "library",
18+
"name": "cyclonedx-python-lib",
19+
"version": "1.0.0"
20+
}
21+
},
22+
"components": []
23+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<bom xmlns="http://cyclonedx.org/schema/bom/1.3" 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+
<component type="library">
13+
<name>cyclonedx-python-lib</name>
14+
<version>1.0.0</version>
15+
</component>
16+
</metadata>
17+
<components/>
18+
</bom>

tests/test_bom.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from unittest import TestCase
2121

2222
from cyclonedx.model.bom import Bom, ThisTool, Tool
23+
from cyclonedx.model.component import Component, ComponentType
2324

2425

2526
class TestBom(TestCase):
@@ -36,3 +37,12 @@ def test_bom_metadata_tool_multiple_tools(self) -> None:
3637
Tool(vendor='TestVendor', name='TestTool', version='0.0.0')
3738
)
3839
self.assertEqual(len(bom.metadata.tools), 2)
40+
41+
def test_metadata_component(self) -> None:
42+
metadata = Bom().metadata
43+
self.assertTrue(metadata.component is None)
44+
hextech = Component(name='Hextech', version='1.0.0',
45+
component_type=ComponentType.LIBRARY)
46+
metadata.component = hextech
47+
self.assertFalse(metadata.component is None)
48+
self.assertEquals(metadata.component, hextech)

tests/test_e2e_environment.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ def setUpClass(cls) -> None:
4949
def test_json_defaults(self) -> None:
5050
outputter: Json = get_instance(bom=TestE2EEnvironment.bom, output_format=OutputFormat.JSON)
5151
bom_json = json.loads(outputter.output_as_string())
52+
self.assertTrue('metadata' in bom_json)
53+
self.assertFalse('component' in bom_json['metadata'])
5254
component_this_library = next(
5355
(x for x in bom_json['components'] if
5456
x['purl'] == 'pkg:pypi/{}@{}'.format(OUR_PACKAGE_NAME, OUR_PACKAGE_VERSION)), None

tests/test_output_json.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
from cyclonedx.model import Encoding, ExternalReference, ExternalReferenceType, HashType, LicenseChoice, Note, \
2626
NoteText, OrganizationalContact, OrganizationalEntity, Property, Tool, XsUri
2727
from cyclonedx.model.bom import Bom
28-
from cyclonedx.model.component import Component
28+
from cyclonedx.model.component import Component, ComponentType
2929
from cyclonedx.model.issue import IssueClassification, IssueType
3030
from cyclonedx.model.release_note import ReleaseNotes
3131
from cyclonedx.model.vulnerability import ImpactAnalysisState, ImpactAnalysisJustification, ImpactAnalysisResponse, \
@@ -327,3 +327,14 @@ def test_simple_bom_v1_4_with_vulnerabilities(self) -> None:
327327
self.assertValidAgainstSchema(bom_json=outputter.output_as_string(), schema_version=SchemaVersion.V1_4)
328328
self.assertEqualJsonBom(expected_json.read(), outputter.output_as_string())
329329
expected_json.close()
330+
331+
def test_bom_v1_3_with_metadata_component(self) -> None:
332+
bom = Bom()
333+
bom.metadata.component = Component(
334+
name='cyclonedx-python-lib', version='1.0.0', component_type=ComponentType.LIBRARY)
335+
outputter = get_instance(bom=bom, output_format=OutputFormat.JSON)
336+
self.assertIsInstance(outputter, JsonV1Dot3)
337+
with open(join(dirname(__file__), 'fixtures/bom_v1.3_with_metadata_component.json')) as expected_json:
338+
self.assertEqualJsonBom(outputter.output_as_string(), expected_json.read())
339+
340+
maxDiff = None

tests/test_output_xml.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
from cyclonedx.model import Encoding, ExternalReference, ExternalReferenceType, HashType, Note, NoteText, \
2626
OrganizationalContact, OrganizationalEntity, Property, Tool, XsUri
2727
from cyclonedx.model.bom import Bom
28-
from cyclonedx.model.component import Component
28+
from cyclonedx.model.component import Component, ComponentType
2929
from cyclonedx.model.impact_analysis import ImpactAnalysisState, ImpactAnalysisJustification, ImpactAnalysisResponse, \
3030
ImpactAnalysisAffectedStatus
3131
from cyclonedx.model.issue import IssueClassification, IssueType
@@ -433,3 +433,14 @@ def test_with_component_release_notes_post_1_4(self) -> None:
433433
self.assertEqualXmlBom(a=outputter.output_as_string(), b=expected_xml.read(),
434434
namespace=outputter.get_target_namespace())
435435
expected_xml.close()
436+
437+
def test_bom_v1_3_with_metadata_component(self) -> None:
438+
bom = Bom()
439+
bom.metadata.component = Component(
440+
name='cyclonedx-python-lib', version='1.0.0', component_type=ComponentType.LIBRARY)
441+
outputter: Xml = get_instance(bom=bom)
442+
self.assertIsInstance(outputter, XmlV1Dot3)
443+
with open(join(dirname(__file__), 'fixtures/bom_v1.3_with_metadata_component.xml')) as expected_xml:
444+
self.assertEqualXmlBom(a=outputter.output_as_string(), b=expected_xml.read(),
445+
namespace=outputter.get_target_namespace())
446+
expected_xml.close()

0 commit comments

Comments
 (0)