Skip to content

Commit

Permalink
Addition of simple 'metadata' element for XML SBOM's.
Browse files Browse the repository at this point in the history
  • Loading branch information
madpah committed Sep 1, 2021
1 parent 3e1f5ec commit f9e9773
Show file tree
Hide file tree
Showing 6 changed files with 70 additions and 9 deletions.
22 changes: 22 additions & 0 deletions cyclonedx/model/bom.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,25 @@
import datetime
from typing import List
from .cyclonedx import Component
from ..parser import BaseParser


class BomMetaData:
"""
Our internal representation of the metadata complex type within the CycloneDX standard.
See https://cyclonedx.org/docs/1.3/#type_metadata
"""

_timestamp: datetime.datetime

def __init__(self):
self._timestamp = datetime.datetime.now(tz=datetime.timezone.utc)

def get_timestamp(self) -> datetime.datetime:
return self._timestamp


class Bom:
"""
This is our internal representation of the BOM.
Expand All @@ -11,6 +28,7 @@ class Bom:
to the requested schema version.
"""

_metadata: BomMetaData = None
_components: List[Component] = []

@staticmethod
Expand All @@ -20,6 +38,7 @@ def from_parser(parser: BaseParser):
return bom

def __init__(self):
self._metadata = BomMetaData()
self._components.clear()

def add_component(self, component: Component):
Expand All @@ -34,5 +53,8 @@ def component_count(self) -> int:
def get_components(self) -> List[Component]:
return self._components

def get_metadata(self) -> BomMetaData:
return self._metadata

def has_component(self, component: Component) -> bool:
return component in self._components
16 changes: 12 additions & 4 deletions cyclonedx/output/xml.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,14 @@ def _xml_pretty_print(elem: ElementTree.Element, level: int = 0) -> ElementTree.


class Xml(BaseOutput):
XML_VERSION_DECLARATION: str = '<?xml version="1.0" encoding="UTF-8"?>\n'
XML_VERSION_DECLARATION: str = '<?xml version="1.0" encoding="UTF-8"?>'

def get_target_namespace(self) -> str:
return 'http://cyclonedx.org/schema/bom/{}'.format(self._get_schema_version())

def output_as_string(self) -> str:
bom = ElementTree.Element('bom', {'xmlns': self._get_target_namespace(), 'version': '1'})
bom = ElementTree.Element('bom', {'xmlns': self.get_target_namespace(), 'version': '1'})
bom = self._add_metadata(bom=bom)
components = ElementTree.SubElement(bom, 'components')
for component in self.get_bom().get_components():
components.append(Xml._get_component_as_xml_element(component=component))
Expand Down Expand Up @@ -83,12 +87,16 @@ def _get_component_as_xml_element(component: Component) -> ElementTree.Element:

return element

def _add_metadata(self, bom: ElementTree.Element) -> ElementTree.Element:
metadata_e = ElementTree.SubElement(bom, 'metadata')
ElementTree.SubElement(metadata_e, 'timestamp').text = self.get_bom().get_metadata().get_timestamp().isoformat()
return bom

@abstractmethod
def _get_schema_version(self) -> str:
pass

def _get_target_namespace(self) -> str:
return 'http://cyclonedx.org/schema/bom/{}'.format(self._get_schema_version())



class XmlV1Dot2(Xml):
Expand Down
25 changes: 24 additions & 1 deletion tests/base.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import xml.etree.ElementTree
from unittest import TestCase

import json
from datetime import datetime, timezone
from xml.dom import minidom


Expand Down Expand Up @@ -28,7 +30,28 @@ class BaseXmlTestCase(TestCase):

def assertEqualXml(self, a: str, b: str):
da, db = minidom.parseString(a), minidom.parseString(b)
self.assertTrue(self._is_equal_xml_element(da.documentElement, db.documentElement))
self.assertTrue(self._is_equal_xml_element(da.documentElement, db.documentElement),
'XML Documents are not equal: \n{}\n{}'.format(da.toxml(), db.toxml()))

def assertEqualXmlBom(self, a: str, b: str, namespace: str):
"""
Sanitise some fields such as timestamps which cannot have their values directly compared for equality.
"""
ba, bb = xml.etree.ElementTree.fromstring(a), xml.etree.ElementTree.fromstring(b)

now = datetime.now(tz=timezone.utc)
metadata_ts_a = ba.find('./{{{}}}metadata/{{{}}}timestamp'.format(namespace, namespace))
if metadata_ts_a is not None:
metadata_ts_a.text = now.isoformat()

metadata_ts_b = bb.find('./{{{}}}metadata/{{{}}}timestamp'.format(namespace, namespace))
if metadata_ts_b is not None:
metadata_ts_b.text = now.isoformat()

self.assertEqualXml(
xml.etree.ElementTree.tostring(ba, 'unicode'),
xml.etree.ElementTree.tostring(bb, 'unicode')
)

def _is_equal_xml_element(self, a, b):
if a.tagName != b.tagName:
Expand Down
3 changes: 3 additions & 0 deletions tests/fixtures/bom_v1.2_setuptools.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<bom xmlns="http://cyclonedx.org/schema/bom/1.2" version="1">
<metadata>
<timestamp>2021-09-01T10:50:42.051979+00:00</timestamp>
</metadata>
<components>
<component type="library" bom-ref="pkg:pypi/setuptools@50.3.2?extension=tar.gz">
<name>setuptools</name>
Expand Down
3 changes: 3 additions & 0 deletions tests/fixtures/bom_v1.3_setuptools.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
<?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>
</metadata>
<components>
<component type="library" bom-ref="pkg:pypi/setuptools@50.3.2?extension=tar.gz">
<name>setuptools</name>
Expand Down
10 changes: 6 additions & 4 deletions tests/test_output_xml.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from cyclonedx.model.bom import Bom
from cyclonedx.model.cyclonedx import Component
from cyclonedx.output import get_instance, SchemaVersion
from cyclonedx.output.xml import XmlV1Dot3, XmlV1Dot2
from cyclonedx.output.xml import XmlV1Dot3, XmlV1Dot2, Xml

from tests.base import BaseXmlTestCase

Expand All @@ -13,10 +13,11 @@ class TestOutputXml(BaseXmlTestCase):
def test_simple_bom_v1_3(self):
bom = Bom()
bom.add_component(Component(name='setuptools', version='50.3.2', qualifiers='extension=tar.gz'))
outputter = get_instance(bom=bom)
outputter: Xml = get_instance(bom=bom)
self.assertIsInstance(outputter, XmlV1Dot3)
with open(join(dirname(__file__), 'fixtures/bom_v1.3_setuptools.xml')) as expected_xml:
self.assertEqualXml(outputter.output_as_string(), expected_xml.read())
self.assertEqualXmlBom(a=outputter.output_as_string(), b=expected_xml.read(),
namespace=outputter.get_target_namespace())
expected_xml.close()

def test_simple_bom_v1_2(self):
Expand All @@ -25,5 +26,6 @@ def test_simple_bom_v1_2(self):
outputter = get_instance(bom=bom, schema_version=SchemaVersion.V1_2)
self.assertIsInstance(outputter, XmlV1Dot2)
with open(join(dirname(__file__), 'fixtures/bom_v1.2_setuptools.xml')) as expected_xml:
self.assertEqualXml(outputter.output_as_string(), expected_xml.read())
self.assertEqualXmlBom(outputter.output_as_string(), expected_xml.read(),
namespace=outputter.get_target_namespace())
expected_xml.close()

0 comments on commit f9e9773

Please sign in to comment.