Skip to content

Commit

Permalink
Merge pull request #20 from CycloneDX/feat/additional-metadata
Browse files Browse the repository at this point in the history
feat: add support for tool(s) that generated the SBOM
  • Loading branch information
madpah authored Oct 11, 2021
2 parents cf13c68 + efc1053 commit b33cbf4
Show file tree
Hide file tree
Showing 14 changed files with 253 additions and 14 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ _Note: We refer throughout using XPath, but the same is true for both XML and JS
<td><code>/bom/metadata</code></td>
<td>Y</td><td>Y</td><td>N/A</td><td>N/A</td>
<td>
Only <code>timestamp</code> is currently supported
<code>timestamp</code> and <code>tools</code> are currently supported
</td>
</tr>
<tr>
Expand Down
46 changes: 46 additions & 0 deletions cyclonedx/model/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,55 @@
# SPDX-License-Identifier: Apache-2.0
#

from enum import Enum

"""
Uniform set of models to represent objects within a CycloneDX software bill-of-materials.
You can either create a `cyclonedx.model.bom.Bom` yourself programmatically, or generate a `cyclonedx.model.bom.Bom`
from a `cyclonedx.parser.BaseParser` implementation.
"""


class HashAlgorithm(Enum):
"""
This is out internal representation of the hashAlg simple type within the CycloneDX standard.
.. note::
See the CycloneDX Schema: https://cyclonedx.org/docs/1.3/#type_hashAlg
"""

BLAKE2B_256 = 'BLAKE2b-256'
BLAKE2B_384 = 'BLAKE2b-384'
BLAKE2B_512 = 'BLAKE2b-512'
BLAKE3 = 'BLAKE3'
MD5 = 'MD5'
SHA_1 = 'SHA-1'
SHA_256 = 'SHA-256'
SHA_384 = 'SHA-384'
SHA_512 = 'SHA-512'
SHA3_256 = 'SHA3-256'
SHA3_384 = 'SHA3-384'
SHA3_512 = 'SHA3-512'


class HashType:
"""
This is out internal representation of the hashType complex type within the CycloneDX standard.
.. note::
See the CycloneDX Schema for hashType: https://cyclonedx.org/docs/1.3/#type_hashType
"""

_algorithm: HashAlgorithm
_value: str

def __init__(self, algorithm: HashAlgorithm, hash_value: str):
self._algorithm = algorithm
self._value = hash_value

def get_algorithm(self) -> HashAlgorithm:
return self._algorithm

def get_hash_value(self) -> str:
return self._value
89 changes: 88 additions & 1 deletion cyclonedx/model/bom.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,87 @@
# Copyright (c) OWASP Foundation. All Rights Reserved.

import datetime
import sys
from typing import List
from uuid import uuid4

from . import HashType
from .component import Component
from ..parser import BaseParser


class Tool:
"""
This is out internal representation of the toolType complex type within the CycloneDX standard.
Tool(s) are the things used in the creation of the BOM.
.. note::
See the CycloneDX Schema for toolType: https://cyclonedx.org/docs/1.3/#type_toolType
"""

_vendor: str = None
_name: str = None
_version: str = None
_hashes: List[HashType] = []

def __init__(self, vendor: str, name: str, version: str, hashes: List[HashType] = []):
self._vendor = vendor
self._name = name
self._version = version
self._hashes = hashes

def get_hashes(self) -> List[HashType]:
"""
List of cryptographic hashes that identify this version of this Tool.
Returns:
`List` of `HashType` objects where there are any hashes, else an empty `List`.
"""
return self._hashes

def get_name(self) -> str:
"""
The name of this Tool.
Returns:
`str` representing the name of the Tool
"""
return self._name

def get_vendor(self) -> str:
"""
The vendor of this Tool.
Returns:
`str` representing the vendor of the Tool
"""
return self._vendor

def get_version(self) -> str:
"""
The version of this Tool.
Returns:
`str` representing the version of the Tool
"""
return self._version

def __repr__(self):
return '<Tool {}:{}:{}>'.format(self._vendor, self._name, self._version)


if sys.version_info >= (3, 8, 0):
from importlib.metadata import version
else:
from importlib_metadata import version

try:
ThisTool = Tool(vendor='CycloneDX', name='cyclonedx-python-lib', version=version('cyclonedx-python-lib'))
except Exception:
ThisTool = Tool(vendor='CycloneDX', name='cyclonedx-python-lib', version='UNKNOWN')


class BomMetaData:
"""
This is our internal representation of the metadata complex type within the CycloneDX standard.
Expand All @@ -34,9 +108,13 @@ class BomMetaData:
"""

_timestamp: datetime.datetime
_tools: List[Tool] = []

def __init__(self):
def __init__(self, tools: List[Tool] = []):
self._timestamp = datetime.datetime.now(tz=datetime.timezone.utc)
if len(tools) == 0:
tools.append(ThisTool)
self._tools = tools

def get_timestamp(self) -> datetime.datetime:
"""
Expand All @@ -47,6 +125,15 @@ def get_timestamp(self) -> datetime.datetime:
"""
return self._timestamp

def get_tools(self) -> List[Tool]:
"""
Tools used to create this BOM.
Returns:
`List` of `Tool` objects where there are any, else an empty `List`.
"""
return self._tools


class Bom:
"""
Expand Down
17 changes: 14 additions & 3 deletions cyclonedx/output/json.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,22 @@ def _get_component_as_dict(self, component: Component) -> dict:
return c

def _get_metadata_as_dict(self) -> dict:
metadata = self.get_bom().get_metadata()
return {
"timestamp": metadata.get_timestamp().isoformat()
bom_metadata = self.get_bom().get_metadata()
metadata = {
"timestamp": bom_metadata.get_timestamp().isoformat()
}

if self.bom_metadata_supports_tools() and len(bom_metadata.get_tools()) > 0:
metadata['tools'] = []
for tool in bom_metadata.get_tools():
metadata['tools'].append({
"vendor": tool.get_vendor(),
"name": tool.get_name(),
"version": tool.get_version()
})

return metadata


class JsonV1Dot0(Json, SchemaVersion1Dot0):
pass
Expand Down
9 changes: 9 additions & 0 deletions cyclonedx/output/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@

class BaseSchemaVersion(ABC):

def bom_metadata_supports_tools(self) -> bool:
return True

def bom_supports_metadata(self) -> bool:
return True

Expand Down Expand Up @@ -49,6 +52,9 @@ def get_schema_version(self) -> str:

class SchemaVersion1Dot1(BaseSchemaVersion):

def bom_metadata_supports_tools(self) -> bool:
return False

def bom_supports_metadata(self) -> bool:
return False

Expand All @@ -61,6 +67,9 @@ def get_schema_version(self) -> str:

class SchemaVersion1Dot0(BaseSchemaVersion):

def bom_metadata_supports_tools(self) -> bool:
return False

def bom_supports_metadata(self) -> bool:
return False

Expand Down
18 changes: 17 additions & 1 deletion cyclonedx/output/xml.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,8 +187,24 @@ def _get_vulnerability_as_xml_element(bom_ref: str, vulnerability: Vulnerability
return vulnerability_element

def _add_metadata(self, bom: ElementTree.Element) -> ElementTree.Element:
bom_metadata = self.get_bom().get_metadata()

metadata_e = ElementTree.SubElement(bom, 'metadata')
ElementTree.SubElement(metadata_e, 'timestamp').text = self.get_bom().get_metadata().get_timestamp().isoformat()
ElementTree.SubElement(metadata_e, 'timestamp').text = bom_metadata.get_timestamp().isoformat()

if self.bom_metadata_supports_tools() and len(bom_metadata.get_tools()) > 0:
tools_e = ElementTree.SubElement(metadata_e, 'tools')
for tool in bom_metadata.get_tools():
tool_e = ElementTree.SubElement(tools_e, 'tool')
ElementTree.SubElement(tool_e, 'vendor').text = tool.get_vendor()
ElementTree.SubElement(tool_e, 'name').text = tool.get_name()
ElementTree.SubElement(tool_e, 'version').text = tool.get_version()
if len(tool.get_hashes()) > 0:
hashes_e = ElementTree.SubElement(tool_e, 'hashes')
for hash in tool.get_hashes():
ElementTree.SubElement(hashes_e, 'hash',
{'alg': hash.get_algorithm().value}).text = hash.get_hash_value()

return bom


Expand Down
13 changes: 8 additions & 5 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

27 changes: 27 additions & 0 deletions tests/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,20 @@
# Copyright (c) OWASP Foundation. All Rights Reserved.

import json
import sys
import xml.etree.ElementTree
from datetime import datetime, timezone
from unittest import TestCase
from uuid import uuid4
from xml.dom import minidom

if sys.version_info >= (3, 8, 0):
from importlib.metadata import version
else:
from importlib_metadata import version

cyclonedx_lib_name: str = 'cyclonedx-python-lib'
cyclonedx_lib_version: str = version(cyclonedx_lib_name)
single_uuid: str = 'urn:uuid:{}'.format(uuid4())


Expand All @@ -50,6 +58,17 @@ def assertEqualJsonBom(self, a: str, b: str):
ab['metadata']['timestamp'] = now.isoformat()
bb['metadata']['timestamp'] = now.isoformat()

# Align 'this' Tool Version
if 'tools' in ab['metadata'].keys():
for i, tool in enumerate(ab['metadata']['tools']):
if tool['name'] == cyclonedx_lib_name:
ab['metadata']['tools'][i]['version'] = cyclonedx_lib_version

if 'tools' in bb['metadata'].keys():
for i, tool in enumerate(bb['metadata']['tools']):
if tool['name'] == cyclonedx_lib_name:
bb['metadata']['tools'][i]['version'] = cyclonedx_lib_version

self.assertEqualJson(json.dumps(ab), json.dumps(bb))


Expand Down Expand Up @@ -80,6 +99,14 @@ def assertEqualXmlBom(self, a: str, b: str, namespace: str):
if metadata_ts_b is not None:
metadata_ts_b.text = now.isoformat()

# Align 'this' Tool Version
this_tool = ba.find('.//*/{{{}}}tool[{{{}}}version="VERSION"]'.format(namespace, namespace))
if this_tool:
this_tool.find('./{{{}}}version'.format(namespace)).text = cyclonedx_lib_version
this_tool = bb.find('.//*/{{{}}}tool[{{{}}}version="VERSION"]'.format(namespace, namespace))
if this_tool:
this_tool.find('./{{{}}}version'.format(namespace)).text = cyclonedx_lib_version

self.assertEqualXml(
xml.etree.ElementTree.tostring(ba, 'unicode'),
xml.etree.ElementTree.tostring(bb, 'unicode')
Expand Down
9 changes: 8 additions & 1 deletion tests/fixtures/bom_v1.2_setuptools.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,14 @@
"serialNumber": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79",
"version": 1,
"metadata": {
"timestamp": "2021-09-01T10:50:42.051979+00:00"
"timestamp": "2021-09-01T10:50:42.051979+00:00",
"tools": [
{
"vendor": "CycloneDX",
"name": "cyclonedx-python-lib",
"version": "VERSION"
}
]
},
"components": [
{
Expand Down
7 changes: 7 additions & 0 deletions tests/fixtures/bom_v1.2_setuptools.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@
<bom xmlns="http://cyclonedx.org/schema/bom/1.2" 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>
</metadata>
<components>
<component type="library" bom-ref="pkg:pypi/[email protected]?extension=tar.gz">
Expand Down
9 changes: 8 additions & 1 deletion tests/fixtures/bom_v1.3_setuptools.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,14 @@
"serialNumber": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79",
"version": 1,
"metadata": {
"timestamp": "2021-09-01T10:50:42.051979+00:00"
"timestamp": "2021-09-01T10:50:42.051979+00:00",
"tools": [
{
"vendor": "CycloneDX",
"name": "cyclonedx-python-lib",
"version": "VERSION"
}
]
},
"components": [
{
Expand Down
Loading

0 comments on commit b33cbf4

Please sign in to comment.