Skip to content

Commit

Permalink
feat: add support for bom.metadata.component (#118)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
artem-smotrakov authored Jan 13, 2022
1 parent 3509fb6 commit 1ac31f4
Show file tree
Hide file tree
Showing 9 changed files with 124 additions and 9 deletions.
26 changes: 26 additions & 0 deletions cyclonedx/model/bom.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ def __init__(self, tools: Optional[List[Tool]] = None) -> None:
if not self.tools:
self.add_tool(ThisTool)

self.component: Optional[Component] = None

@property
def tools(self) -> List[Tool]:
"""
Expand Down Expand Up @@ -80,6 +82,30 @@ def timestamp(self) -> datetime:
def timestamp(self, timestamp: datetime) -> None:
self._timestamp = timestamp

@property
def component(self) -> Optional[Component]:
"""
The (optional) component that the BOM describes.
Returns:
`cyclonedx.model.component.Component` instance for this Bom Metadata.
"""
return self._component

@component.setter
def component(self, component: Component) -> None:
"""
The (optional) component that the BOM describes.
Args:
component
`cyclonedx.model.component.Component` instance to add to this Bom Metadata.
Returns:
None
"""
self._component = component


class Bom:
"""
Expand Down
25 changes: 18 additions & 7 deletions cyclonedx/output/json.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@
from ..model.bom import Bom


ComponentDict = Dict[str, Union[
str,
List[Dict[str, str]],
List[Dict[str, Dict[str, str]]],
List[Dict[str, Union[str, List[Dict[str, str]]]]]]]


class Json(BaseOutput, BaseSchemaVersion):

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

# Iterate Components
for i in range(len(bom_json['components'])):
if not self.component_supports_author() and 'author' in bom_json['components'][i].keys():
del bom_json['components'][i]['author']
if 'components' in bom_json.keys():
for i in range(len(bom_json['components'])):
if not self.component_supports_author() and 'author' in bom_json['components'][i].keys():
del bom_json['components'][i]['author']

if not self.component_supports_mime_type_attribute() and 'mime-type' in bom_json['components'][i].keys():
del bom_json['components'][i]['mime-type']
if not self.component_supports_mime_type_attribute() \
and 'mime-type' in bom_json['components'][i].keys():
del bom_json['components'][i]['mime-type']

if not self.component_supports_release_notes() and 'releaseNotes' in bom_json['components'][i].keys():
del bom_json['components'][i]['releaseNotes']
if not self.component_supports_release_notes() and 'releaseNotes' in bom_json['components'][i].keys():
del bom_json['components'][i]['releaseNotes']
else:
bom_json['components'] = []

# Iterate Vulnerabilities
if 'vulnerabilities' in bom_json.keys():
Expand Down
3 changes: 3 additions & 0 deletions cyclonedx/output/xml.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,9 @@ def _add_metadata_element(self) -> None:
for tool in bom_metadata.tools:
self._add_tool(parent_element=tools_e, tool=tool)

if bom_metadata.component:
metadata_e.append(self._add_component_element(component=bom_metadata.component))

def _add_component_element(self, component: Component) -> ElementTree.Element:
element_attributes = {'type': component.type.value}
if self.component_supports_bom_ref_attribute() and component.bom_ref:
Expand Down
23 changes: 23 additions & 0 deletions tests/fixtures/bom_v1.3_with_metadata_component.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"$schema": "http://cyclonedx.org/schema/bom-1.3.schema.json",
"bomFormat": "CycloneDX",
"specVersion": "1.3",
"serialNumber": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79",
"version": 1,
"metadata": {
"timestamp": "2021-09-01T10:50:42.051979+00:00",
"tools": [
{
"vendor": "CycloneDX",
"name": "cyclonedx-python-lib",
"version": "VERSION"
}
],
"component": {
"type": "library",
"name": "cyclonedx-python-lib",
"version": "1.0.0"
}
},
"components": []
}
18 changes: 18 additions & 0 deletions tests/fixtures/bom_v1.3_with_metadata_component.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<bom xmlns="http://cyclonedx.org/schema/bom/1.3" version="1">
<metadata>
<timestamp>2021-09-01T10:50:42.051979+00:00</timestamp>
<tools>
<tool>
<vendor>CycloneDX</vendor>
<name>cyclonedx-python-lib</name>
<version>VERSION</version>
</tool>
</tools>
<component type="library">
<name>cyclonedx-python-lib</name>
<version>1.0.0</version>
</component>
</metadata>
<components/>
</bom>
10 changes: 10 additions & 0 deletions tests/test_bom.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from unittest import TestCase

from cyclonedx.model.bom import Bom, ThisTool, Tool
from cyclonedx.model.component import Component, ComponentType


class TestBom(TestCase):
Expand All @@ -36,3 +37,12 @@ def test_bom_metadata_tool_multiple_tools(self) -> None:
Tool(vendor='TestVendor', name='TestTool', version='0.0.0')
)
self.assertEqual(len(bom.metadata.tools), 2)

def test_metadata_component(self) -> None:
metadata = Bom().metadata
self.assertTrue(metadata.component is None)
hextech = Component(name='Hextech', version='1.0.0',
component_type=ComponentType.LIBRARY)
metadata.component = hextech
self.assertFalse(metadata.component is None)
self.assertEquals(metadata.component, hextech)
2 changes: 2 additions & 0 deletions tests/test_e2e_environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ def setUpClass(cls) -> None:
def test_json_defaults(self) -> None:
outputter: Json = get_instance(bom=TestE2EEnvironment.bom, output_format=OutputFormat.JSON)
bom_json = json.loads(outputter.output_as_string())
self.assertTrue('metadata' in bom_json)
self.assertFalse('component' in bom_json['metadata'])
component_this_library = next(
(x for x in bom_json['components'] if
x['purl'] == 'pkg:pypi/{}@{}'.format(OUR_PACKAGE_NAME, OUR_PACKAGE_VERSION)), None
Expand Down
13 changes: 12 additions & 1 deletion tests/test_output_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
from cyclonedx.model import Encoding, ExternalReference, ExternalReferenceType, HashType, LicenseChoice, Note, \
NoteText, OrganizationalContact, OrganizationalEntity, Property, Tool, XsUri
from cyclonedx.model.bom import Bom
from cyclonedx.model.component import Component
from cyclonedx.model.component import Component, ComponentType
from cyclonedx.model.issue import IssueClassification, IssueType
from cyclonedx.model.release_note import ReleaseNotes
from cyclonedx.model.vulnerability import ImpactAnalysisState, ImpactAnalysisJustification, ImpactAnalysisResponse, \
Expand Down Expand Up @@ -327,3 +327,14 @@ def test_simple_bom_v1_4_with_vulnerabilities(self) -> None:
self.assertValidAgainstSchema(bom_json=outputter.output_as_string(), schema_version=SchemaVersion.V1_4)
self.assertEqualJsonBom(expected_json.read(), outputter.output_as_string())
expected_json.close()

def test_bom_v1_3_with_metadata_component(self) -> None:
bom = Bom()
bom.metadata.component = Component(
name='cyclonedx-python-lib', version='1.0.0', component_type=ComponentType.LIBRARY)
outputter = get_instance(bom=bom, output_format=OutputFormat.JSON)
self.assertIsInstance(outputter, JsonV1Dot3)
with open(join(dirname(__file__), 'fixtures/bom_v1.3_with_metadata_component.json')) as expected_json:
self.assertEqualJsonBom(outputter.output_as_string(), expected_json.read())

maxDiff = None
13 changes: 12 additions & 1 deletion tests/test_output_xml.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
from cyclonedx.model import Encoding, ExternalReference, ExternalReferenceType, HashType, Note, NoteText, \
OrganizationalContact, OrganizationalEntity, Property, Tool, XsUri
from cyclonedx.model.bom import Bom
from cyclonedx.model.component import Component
from cyclonedx.model.component import Component, ComponentType
from cyclonedx.model.impact_analysis import ImpactAnalysisState, ImpactAnalysisJustification, ImpactAnalysisResponse, \
ImpactAnalysisAffectedStatus
from cyclonedx.model.issue import IssueClassification, IssueType
Expand Down Expand Up @@ -433,3 +433,14 @@ def test_with_component_release_notes_post_1_4(self) -> None:
self.assertEqualXmlBom(a=outputter.output_as_string(), b=expected_xml.read(),
namespace=outputter.get_target_namespace())
expected_xml.close()

def test_bom_v1_3_with_metadata_component(self) -> None:
bom = Bom()
bom.metadata.component = Component(
name='cyclonedx-python-lib', version='1.0.0', component_type=ComponentType.LIBRARY)
outputter: Xml = get_instance(bom=bom)
self.assertIsInstance(outputter, XmlV1Dot3)
with open(join(dirname(__file__), 'fixtures/bom_v1.3_with_metadata_component.xml')) as expected_xml:
self.assertEqualXmlBom(a=outputter.output_as_string(), b=expected_xml.read(),
namespace=outputter.get_target_namespace())
expected_xml.close()

0 comments on commit 1ac31f4

Please sign in to comment.