Skip to content

Commit 7e0fb3c

Browse files
committed
feat: helper method for representing a File as a Component taking into account versioning for files as per CycloneDX/cyclonedx.org-website#34
Signed-off-by: Paul Horton <[email protected]>
1 parent fde79e0 commit 7e0fb3c

File tree

5 files changed

+108
-26
lines changed

5 files changed

+108
-26
lines changed

cyclonedx/model/__init__.py

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

18+
import hashlib
1819
from enum import Enum
1920

2021
"""
@@ -25,6 +26,16 @@
2526
"""
2627

2728

29+
def sha1sum(filename: str) -> str:
30+
h = hashlib.sha1()
31+
b = bytearray(128 * 1024)
32+
mv = memoryview(b)
33+
with open(filename, 'rb', buffering=0) as f:
34+
for n in iter(lambda: f.readinto(mv), 0):
35+
h.update(mv[:n])
36+
return h.hexdigest()
37+
38+
2839
class HashAlgorithm(Enum):
2940
"""
3041
This is out internal representation of the hashAlg simple type within the CycloneDX standard.

cyclonedx/model/component.py

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

2020
from enum import Enum
21+
from os.path import exists
2122
from packageurl import PackageURL
2223
from typing import List
2324

25+
from . import HashAlgorithm, HashType, sha1sum
2426
from .vulnerability import Vulnerability
2527

2628

@@ -58,17 +60,58 @@ class Component:
5860
_description: str = None
5961
_license: str = None
6062

63+
_hashes: List[HashType] = []
6164
_vulnerabilites: List[Vulnerability] = []
6265

63-
def __init__(self, name: str, version: str, qualifiers: str = None,
66+
@staticmethod
67+
def for_file(absolute_file_path: str, path_for_bom: str = None):
68+
"""
69+
Helper method to create a Component that represents the provided local file as a Component.
70+
71+
Args:
72+
absolute_file_path:
73+
Absolute path to the file you wish to represent
74+
path_for_bom:
75+
Optionally, if supplied this is the path that will be used to identify the file in the BOM
76+
77+
Returns:
78+
`Component` representing the supplied file
79+
"""
80+
if not exists(absolute_file_path):
81+
raise FileExistsError('Supplied file path \'{}\' does not exist'.format(absolute_file_path))
82+
83+
sha1_hash: str = sha1sum(filename=absolute_file_path)
84+
85+
return Component(
86+
name=path_for_bom if path_for_bom else absolute_file_path,
87+
version='0.0.0-{}'.format(sha1_hash[0:12]),
88+
hashes=[
89+
HashType(algorithm=HashAlgorithm.SHA_1, hash_value=sha1_hash)
90+
],
91+
component_type=ComponentType.FILE,
92+
package_url_type='generic'
93+
)
94+
95+
def __init__(self, name: str, version: str, qualifiers: str = None, hashes: List[HashType] = [],
6496
component_type: ComponentType = ComponentType.LIBRARY, package_url_type: str = 'pypi'):
6597
self._name = name
6698
self._version = version
6799
self._type = component_type
68100
self._qualifiers = qualifiers
101+
self._hashes = hashes
69102
self._vulnerabilites = []
70103
self._package_url_type = package_url_type
71104

105+
def add_hash(self, hash: HashType):
106+
"""
107+
Adds a hash that pins/identifies this Component.
108+
109+
Args:
110+
hash:
111+
`HashType` instance
112+
"""
113+
self._hashes.append(hash)
114+
72115
def add_vulnerability(self, vulnerability: Vulnerability):
73116
"""
74117
Add a Vulnerability to this Component.
@@ -100,6 +143,15 @@ def get_description(self) -> str:
100143
"""
101144
return self._description
102145

146+
def get_hashes(self) -> List[HashType]:
147+
"""
148+
List of cryptographic hashes that identify this Component.
149+
150+
Returns:
151+
`List` of `HashType` objects where there are any hashes, else an empty `List`.
152+
"""
153+
return self._hashes
154+
103155
def get_license(self) -> str:
104156
"""
105157
Get the license of this Component.

cyclonedx/output/json.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,15 @@ def _get_component_as_dict(self, component: Component) -> dict:
5353
"purl": component.get_purl()
5454
}
5555

56+
if len(component.get_hashes()) > 0:
57+
hashes = []
58+
for component_hash in component.get_hashes():
59+
hashes.append({
60+
"alg": component_hash.get_algorithm().value,
61+
"content": component_hash.get_hash_value()
62+
})
63+
c['hashes'] = hashes
64+
5665
if self.component_supports_author() and component.get_author() is not None:
5766
c['author'] = component.get_author()
5867

@@ -67,11 +76,22 @@ def _get_metadata_as_dict(self) -> dict:
6776
if self.bom_metadata_supports_tools() and len(bom_metadata.get_tools()) > 0:
6877
metadata['tools'] = []
6978
for tool in bom_metadata.get_tools():
70-
metadata['tools'].append({
79+
tool_dict = {
7180
"vendor": tool.get_vendor(),
7281
"name": tool.get_name(),
7382
"version": tool.get_version()
74-
})
83+
}
84+
85+
if len(tool.get_hashes()) > 0:
86+
hashes = []
87+
for tool_hash in tool.get_hashes():
88+
hashes.append({
89+
"alg": tool_hash.get_algorithm().value,
90+
"content": tool_hash.get_hash_value()
91+
})
92+
tool_dict['hashes'] = hashes
93+
94+
metadata['tools'].append(tool_dict)
7595

7696
return metadata
7797

cyclonedx/output/xml.py

Lines changed: 10 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -83,35 +83,22 @@ def _get_component_as_xml_element(self, component: Component) -> ElementTree.Ele
8383
if self.component_supports_author() and component.get_author() is not None:
8484
ElementTree.SubElement(component_element, 'author').text = component.get_author()
8585

86-
# if publisher and publisher != "UNKNOWN":
87-
# ElementTree.SubElement(component, "publisher").text = re.sub(RE_XML_ILLEGAL, "?", publisher)
88-
89-
# if name and name != "UNKNOWN":
86+
# name
9087
ElementTree.SubElement(component_element, 'name').text = component.get_name()
9188

92-
# if version and version != "UNKNOWN":
89+
# version
9390
ElementTree.SubElement(component_element, 'version').text = component.get_version()
9491

95-
# if description and description != "UNKNOWN":
96-
# ElementTree.SubElement(component, "description").text = re.sub(RE_XML_ILLEGAL, "?", description)
97-
#
98-
# if hashes:
99-
# hashes_elm = ElementTree.SubElement(component, "hashes")
100-
# for h in hashes:
101-
# ElementTree.SubElement(hashes_elm, "hash", alg=h.alg).text = h.content
102-
#
103-
# if len(licenses):
104-
# licenses_elm = ElementTree.SubElement(component, "licenses")
105-
# for component_license in licenses:
106-
# if component_license.license is not None:
107-
# license_elm = ElementTree.SubElement(licenses_elm, "license")
108-
# ElementTree.SubElement(license_elm, "name").text = re.sub(RE_XML_ILLEGAL, "?",
109-
# component_license.license.name)
110-
111-
# if purl:
92+
# purl
11293
ElementTree.SubElement(component_element, 'purl').text = component.get_purl()
11394

114-
# ElementTree.SubElement(component, "modified").text = modified if modified else "false"
95+
# hashes
96+
if len(component.get_hashes()) > 0:
97+
hashes_e = ElementTree.SubElement(component_element, 'hashes')
98+
for hash in component.get_hashes():
99+
ElementTree.SubElement(
100+
hashes_e, 'hash', {'alg': hash.get_algorithm().value}
101+
).text = hash.get_hash_value()
115102

116103
return component_element
117104

tests/test_component.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
# SPDX-License-Identifier: Apache-2.0
1818
# Copyright (c) OWASP Foundation. All Rights Reserved.
1919

20+
from os.path import dirname, join
2021
from unittest import TestCase
2122

2223
from packageurl import PackageURL
@@ -104,3 +105,14 @@ def test_custom_package_url_type(self):
104105
type='generic', name='/test.py', version='UNKNOWN'
105106
)
106107
self.assertEqual(TestComponent._component_generic_file.to_package_url(), purl)
108+
109+
def test_from_file_with_path_for_bom(self):
110+
test_file = join(dirname(__file__), 'fixtures/bom_v1.3_setuptools.xml')
111+
c = Component.for_file(absolute_file_path=test_file, path_for_bom='fixtures/bom_v1.3_setuptools.xml')
112+
self.assertEqual(c.get_name(), 'fixtures/bom_v1.3_setuptools.xml')
113+
self.assertEqual(c.get_version(), '0.0.0-16932e52ed1e')
114+
purl = PackageURL(
115+
type='generic', name='fixtures/bom_v1.3_setuptools.xml', version='0.0.0-16932e52ed1e'
116+
)
117+
self.assertEqual(c.to_package_url(), purl)
118+
self.assertEqual(len(c.get_hashes()), 1)

0 commit comments

Comments
 (0)