Skip to content

Commit b33cbf4

Browse files
authored
Merge pull request #20 from CycloneDX/feat/additional-metadata
feat: add support for tool(s) that generated the SBOM
2 parents cf13c68 + efc1053 commit b33cbf4

14 files changed

+253
-14
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ _Note: We refer throughout using XPath, but the same is true for both XML and JS
161161
<td><code>/bom/metadata</code></td>
162162
<td>Y</td><td>Y</td><td>N/A</td><td>N/A</td>
163163
<td>
164-
Only <code>timestamp</code> is currently supported
164+
<code>timestamp</code> and <code>tools</code> are currently supported
165165
</td>
166166
</tr>
167167
<tr>

cyclonedx/model/__init__.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,55 @@
1515
# SPDX-License-Identifier: Apache-2.0
1616
#
1717

18+
from enum import Enum
19+
1820
"""
1921
Uniform set of models to represent objects within a CycloneDX software bill-of-materials.
2022
2123
You can either create a `cyclonedx.model.bom.Bom` yourself programmatically, or generate a `cyclonedx.model.bom.Bom`
2224
from a `cyclonedx.parser.BaseParser` implementation.
2325
"""
26+
27+
28+
class HashAlgorithm(Enum):
29+
"""
30+
This is out internal representation of the hashAlg simple type within the CycloneDX standard.
31+
32+
.. note::
33+
See the CycloneDX Schema: https://cyclonedx.org/docs/1.3/#type_hashAlg
34+
"""
35+
36+
BLAKE2B_256 = 'BLAKE2b-256'
37+
BLAKE2B_384 = 'BLAKE2b-384'
38+
BLAKE2B_512 = 'BLAKE2b-512'
39+
BLAKE3 = 'BLAKE3'
40+
MD5 = 'MD5'
41+
SHA_1 = 'SHA-1'
42+
SHA_256 = 'SHA-256'
43+
SHA_384 = 'SHA-384'
44+
SHA_512 = 'SHA-512'
45+
SHA3_256 = 'SHA3-256'
46+
SHA3_384 = 'SHA3-384'
47+
SHA3_512 = 'SHA3-512'
48+
49+
50+
class HashType:
51+
"""
52+
This is out internal representation of the hashType complex type within the CycloneDX standard.
53+
54+
.. note::
55+
See the CycloneDX Schema for hashType: https://cyclonedx.org/docs/1.3/#type_hashType
56+
"""
57+
58+
_algorithm: HashAlgorithm
59+
_value: str
60+
61+
def __init__(self, algorithm: HashAlgorithm, hash_value: str):
62+
self._algorithm = algorithm
63+
self._value = hash_value
64+
65+
def get_algorithm(self) -> HashAlgorithm:
66+
return self._algorithm
67+
68+
def get_hash_value(self) -> str:
69+
return self._value

cyclonedx/model/bom.py

Lines changed: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,87 @@
1818
# Copyright (c) OWASP Foundation. All Rights Reserved.
1919

2020
import datetime
21+
import sys
2122
from typing import List
2223
from uuid import uuid4
2324

25+
from . import HashType
2426
from .component import Component
2527
from ..parser import BaseParser
2628

2729

30+
class Tool:
31+
"""
32+
This is out internal representation of the toolType complex type within the CycloneDX standard.
33+
34+
Tool(s) are the things used in the creation of the BOM.
35+
36+
.. note::
37+
See the CycloneDX Schema for toolType: https://cyclonedx.org/docs/1.3/#type_toolType
38+
"""
39+
40+
_vendor: str = None
41+
_name: str = None
42+
_version: str = None
43+
_hashes: List[HashType] = []
44+
45+
def __init__(self, vendor: str, name: str, version: str, hashes: List[HashType] = []):
46+
self._vendor = vendor
47+
self._name = name
48+
self._version = version
49+
self._hashes = hashes
50+
51+
def get_hashes(self) -> List[HashType]:
52+
"""
53+
List of cryptographic hashes that identify this version of this Tool.
54+
55+
Returns:
56+
`List` of `HashType` objects where there are any hashes, else an empty `List`.
57+
"""
58+
return self._hashes
59+
60+
def get_name(self) -> str:
61+
"""
62+
The name of this Tool.
63+
64+
Returns:
65+
`str` representing the name of the Tool
66+
"""
67+
return self._name
68+
69+
def get_vendor(self) -> str:
70+
"""
71+
The vendor of this Tool.
72+
73+
Returns:
74+
`str` representing the vendor of the Tool
75+
"""
76+
return self._vendor
77+
78+
def get_version(self) -> str:
79+
"""
80+
The version of this Tool.
81+
82+
Returns:
83+
`str` representing the version of the Tool
84+
"""
85+
return self._version
86+
87+
def __repr__(self):
88+
return '<Tool {}:{}:{}>'.format(self._vendor, self._name, self._version)
89+
90+
91+
if sys.version_info >= (3, 8, 0):
92+
from importlib.metadata import version
93+
else:
94+
from importlib_metadata import version
95+
96+
try:
97+
ThisTool = Tool(vendor='CycloneDX', name='cyclonedx-python-lib', version=version('cyclonedx-python-lib'))
98+
except Exception:
99+
ThisTool = Tool(vendor='CycloneDX', name='cyclonedx-python-lib', version='UNKNOWN')
100+
101+
28102
class BomMetaData:
29103
"""
30104
This is our internal representation of the metadata complex type within the CycloneDX standard.
@@ -34,9 +108,13 @@ class BomMetaData:
34108
"""
35109

36110
_timestamp: datetime.datetime
111+
_tools: List[Tool] = []
37112

38-
def __init__(self):
113+
def __init__(self, tools: List[Tool] = []):
39114
self._timestamp = datetime.datetime.now(tz=datetime.timezone.utc)
115+
if len(tools) == 0:
116+
tools.append(ThisTool)
117+
self._tools = tools
40118

41119
def get_timestamp(self) -> datetime.datetime:
42120
"""
@@ -47,6 +125,15 @@ def get_timestamp(self) -> datetime.datetime:
47125
"""
48126
return self._timestamp
49127

128+
def get_tools(self) -> List[Tool]:
129+
"""
130+
Tools used to create this BOM.
131+
132+
Returns:
133+
`List` of `Tool` objects where there are any, else an empty `List`.
134+
"""
135+
return self._tools
136+
50137

51138
class Bom:
52139
"""

cyclonedx/output/json.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,11 +59,22 @@ def _get_component_as_dict(self, component: Component) -> dict:
5959
return c
6060

6161
def _get_metadata_as_dict(self) -> dict:
62-
metadata = self.get_bom().get_metadata()
63-
return {
64-
"timestamp": metadata.get_timestamp().isoformat()
62+
bom_metadata = self.get_bom().get_metadata()
63+
metadata = {
64+
"timestamp": bom_metadata.get_timestamp().isoformat()
6565
}
6666

67+
if self.bom_metadata_supports_tools() and len(bom_metadata.get_tools()) > 0:
68+
metadata['tools'] = []
69+
for tool in bom_metadata.get_tools():
70+
metadata['tools'].append({
71+
"vendor": tool.get_vendor(),
72+
"name": tool.get_name(),
73+
"version": tool.get_version()
74+
})
75+
76+
return metadata
77+
6778

6879
class JsonV1Dot0(Json, SchemaVersion1Dot0):
6980
pass

cyclonedx/output/schema.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@
2222

2323
class BaseSchemaVersion(ABC):
2424

25+
def bom_metadata_supports_tools(self) -> bool:
26+
return True
27+
2528
def bom_supports_metadata(self) -> bool:
2629
return True
2730

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

5053
class SchemaVersion1Dot1(BaseSchemaVersion):
5154

55+
def bom_metadata_supports_tools(self) -> bool:
56+
return False
57+
5258
def bom_supports_metadata(self) -> bool:
5359
return False
5460

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

6268
class SchemaVersion1Dot0(BaseSchemaVersion):
6369

70+
def bom_metadata_supports_tools(self) -> bool:
71+
return False
72+
6473
def bom_supports_metadata(self) -> bool:
6574
return False
6675

cyclonedx/output/xml.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,8 +187,24 @@ def _get_vulnerability_as_xml_element(bom_ref: str, vulnerability: Vulnerability
187187
return vulnerability_element
188188

189189
def _add_metadata(self, bom: ElementTree.Element) -> ElementTree.Element:
190+
bom_metadata = self.get_bom().get_metadata()
191+
190192
metadata_e = ElementTree.SubElement(bom, 'metadata')
191-
ElementTree.SubElement(metadata_e, 'timestamp').text = self.get_bom().get_metadata().get_timestamp().isoformat()
193+
ElementTree.SubElement(metadata_e, 'timestamp').text = bom_metadata.get_timestamp().isoformat()
194+
195+
if self.bom_metadata_supports_tools() and len(bom_metadata.get_tools()) > 0:
196+
tools_e = ElementTree.SubElement(metadata_e, 'tools')
197+
for tool in bom_metadata.get_tools():
198+
tool_e = ElementTree.SubElement(tools_e, 'tool')
199+
ElementTree.SubElement(tool_e, 'vendor').text = tool.get_vendor()
200+
ElementTree.SubElement(tool_e, 'name').text = tool.get_name()
201+
ElementTree.SubElement(tool_e, 'version').text = tool.get_version()
202+
if len(tool.get_hashes()) > 0:
203+
hashes_e = ElementTree.SubElement(tool_e, 'hashes')
204+
for hash in tool.get_hashes():
205+
ElementTree.SubElement(hashes_e, 'hash',
206+
{'alg': hash.get_algorithm().value}).text = hash.get_hash_value()
207+
192208
return bom
193209

194210

poetry.lock

Lines changed: 8 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/base.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,20 @@
1818
# Copyright (c) OWASP Foundation. All Rights Reserved.
1919

2020
import json
21+
import sys
2122
import xml.etree.ElementTree
2223
from datetime import datetime, timezone
2324
from unittest import TestCase
2425
from uuid import uuid4
2526
from xml.dom import minidom
2627

28+
if sys.version_info >= (3, 8, 0):
29+
from importlib.metadata import version
30+
else:
31+
from importlib_metadata import version
32+
33+
cyclonedx_lib_name: str = 'cyclonedx-python-lib'
34+
cyclonedx_lib_version: str = version(cyclonedx_lib_name)
2735
single_uuid: str = 'urn:uuid:{}'.format(uuid4())
2836

2937

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

61+
# Align 'this' Tool Version
62+
if 'tools' in ab['metadata'].keys():
63+
for i, tool in enumerate(ab['metadata']['tools']):
64+
if tool['name'] == cyclonedx_lib_name:
65+
ab['metadata']['tools'][i]['version'] = cyclonedx_lib_version
66+
67+
if 'tools' in bb['metadata'].keys():
68+
for i, tool in enumerate(bb['metadata']['tools']):
69+
if tool['name'] == cyclonedx_lib_name:
70+
bb['metadata']['tools'][i]['version'] = cyclonedx_lib_version
71+
5372
self.assertEqualJson(json.dumps(ab), json.dumps(bb))
5473

5574

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

102+
# Align 'this' Tool Version
103+
this_tool = ba.find('.//*/{{{}}}tool[{{{}}}version="VERSION"]'.format(namespace, namespace))
104+
if this_tool:
105+
this_tool.find('./{{{}}}version'.format(namespace)).text = cyclonedx_lib_version
106+
this_tool = bb.find('.//*/{{{}}}tool[{{{}}}version="VERSION"]'.format(namespace, namespace))
107+
if this_tool:
108+
this_tool.find('./{{{}}}version'.format(namespace)).text = cyclonedx_lib_version
109+
83110
self.assertEqualXml(
84111
xml.etree.ElementTree.tostring(ba, 'unicode'),
85112
xml.etree.ElementTree.tostring(bb, 'unicode')

tests/fixtures/bom_v1.2_setuptools.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,14 @@
44
"serialNumber": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79",
55
"version": 1,
66
"metadata": {
7-
"timestamp": "2021-09-01T10:50:42.051979+00:00"
7+
"timestamp": "2021-09-01T10:50:42.051979+00:00",
8+
"tools": [
9+
{
10+
"vendor": "CycloneDX",
11+
"name": "cyclonedx-python-lib",
12+
"version": "VERSION"
13+
}
14+
]
815
},
916
"components": [
1017
{

tests/fixtures/bom_v1.2_setuptools.xml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@
22
<bom xmlns="http://cyclonedx.org/schema/bom/1.2" version="1">
33
<metadata>
44
<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>
512
</metadata>
613
<components>
714
<component type="library" bom-ref="pkg:pypi/[email protected]?extension=tar.gz">

tests/fixtures/bom_v1.3_setuptools.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,14 @@
44
"serialNumber": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79",
55
"version": 1,
66
"metadata": {
7-
"timestamp": "2021-09-01T10:50:42.051979+00:00"
7+
"timestamp": "2021-09-01T10:50:42.051979+00:00",
8+
"tools": [
9+
{
10+
"vendor": "CycloneDX",
11+
"name": "cyclonedx-python-lib",
12+
"version": "VERSION"
13+
}
14+
]
815
},
916
"components": [
1017
{

0 commit comments

Comments
 (0)