Skip to content

Commit 3953bb6

Browse files
authored
feat: bom-ref for Component and Vulnerability default to a UUID (#142)
* feat: `bom-ref` for Component and Vulnerability default to a UUID if not supplied ensuring they have a unique value #141 Signed-off-by: Paul Horton <[email protected]> * doc: updated documentation to reflect change Signed-off-by: Paul Horton <[email protected]> * patched other tests to support UUID for bom-ref Signed-off-by: Paul Horton <[email protected]> * better syntax Signed-off-by: Paul Horton <[email protected]>
1 parent 97c215c commit 3953bb6

9 files changed

+80
-17
lines changed

cyclonedx/model/component.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from enum import Enum
2222
from os.path import exists
2323
from typing import List, Optional
24+
from uuid import uuid4
2425

2526
# See https://github.com/package-url/packageurl-python/issues/65
2627
from packageurl import PackageURL # type: ignore
@@ -112,7 +113,7 @@ def __init__(self, name: str, component_type: ComponentType = ComponentType.LIBR
112113
) -> None:
113114
self.type = component_type
114115
self.mime_type = mime_type
115-
self.bom_ref = bom_ref
116+
self.bom_ref = bom_ref or str(uuid4())
116117
self.supplier = supplier
117118
self.author = author
118119
self.publisher = publisher
@@ -189,8 +190,10 @@ def bom_ref(self) -> Optional[str]:
189190
An optional identifier which can be used to reference the component elsewhere in the BOM. Every bom-ref MUST be
190191
unique within the BOM.
191192
193+
If a value was not provided in the constructor, a UUIDv4 will have been assigned.
194+
192195
Returns:
193-
`str` as a unique identifiers for this Component if set else `None`
196+
`str` as a unique identifiers for this Component
194197
"""
195198
return self._bom_ref
196199

@@ -507,7 +510,7 @@ def __eq__(self, other: object) -> bool:
507510

508511
def __hash__(self) -> int:
509512
return hash((
510-
self.author, self.bom_ref, self.copyright, self.description, str(self.external_references), self.group,
513+
self.author, self.copyright, self.description, str(self.external_references), self.group,
511514
str(self.hashes), str(self.licenses), self.mime_type, self.name, self.properties, self.publisher, self.purl,
512515
self.release_notes, self.scope, self.supplier, self.type, self.version, self.cpe
513516
))

cyclonedx/model/vulnerability.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
from enum import Enum
2525
from typing import List, Optional, Tuple, Union
2626
from urllib.parse import ParseResult, urlparse
27+
from uuid import uuid4
2728

2829
from . import OrganizationalContact, OrganizationalEntity, Tool, XsUri
2930
from .impact_analysis import ImpactAnalysisAffectedStatus, ImpactAnalysisJustification, ImpactAnalysisResponse, \
@@ -644,7 +645,7 @@ def __init__(self, bom_ref: Optional[str] = None, id: Optional[str] = None,
644645
# Deprecated Parameters kept for backwards compatibility
645646
source_name: Optional[str] = None, source_url: Optional[str] = None,
646647
recommendations: Optional[List[str]] = None) -> None:
647-
self.bom_ref = bom_ref
648+
self.bom_ref = bom_ref or str(uuid4())
648649
self.id = id
649650
self.source = source
650651
self.references = references or []
@@ -677,8 +678,10 @@ def bom_ref(self) -> Optional[str]:
677678
"""
678679
Get the unique reference for this Vulnerability in this BOM.
679680
681+
If a value was not provided in the constructor, a UUIDv4 will have been assigned.
682+
680683
Returns:
681-
`str` if set else `None`
684+
`str` unique identifier for this Vulnerability
682685
"""
683686
return self._bom_ref
684687

docs/modelling.rst

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,17 @@ Examples
1515
From a Parser
1616
~~~~~~~~~~~~~
1717

18+
**Note:** Concreate parser implementations were moved out of this library and into `CycloneDX Python`_ as of version
19+
``1.0.0``.
20+
1821
.. code-block:: python
1922
2023
from cyclonedx.model.bom import Bom
21-
from cyclonedx.parser.environment import EnvironmentParser
24+
from cyclonedx_py.parser.environment import EnvironmentParser
2225
2326
parser = EnvironmentParser()
2427
bom = Bom.from_parser(parser=parser)
2528
2629
30+
.. _CycloneDX Python: https://github.com/CycloneDX/cyclonedx-python
2731
.. _Jake: https://pypi.org/project/jake

tests/fixtures/bom_v1.3_with_metadata_component.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
}
1515
],
1616
"component": {
17+
"bom-ref": "be2c6502-7e9a-47db-9a66-e34f729810a3",
1718
"type": "library",
1819
"name": "cyclonedx-python-lib",
1920
"version": "1.0.0"

tests/fixtures/bom_v1.3_with_metadata_component.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
<version>VERSION</version>
1010
</tool>
1111
</tools>
12-
<component type="library">
12+
<component type="library" bom-ref="5d82790b-3139-431d-855a-ab63d14a18bb">
1313
<name>cyclonedx-python-lib</name>
1414
<version>1.0.0</version>
1515
</component>

tests/test_model_component.py

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,41 @@
11
from unittest import TestCase
2+
from unittest.mock import Mock, patch
23

34
from cyclonedx.model import ExternalReference, ExternalReferenceType
45
from cyclonedx.model.component import Component, ComponentType
56

67

78
class TestModelComponent(TestCase):
89

9-
def test_empty_basic_component(self) -> None:
10+
@patch('cyclonedx.model.component.uuid4', return_value='6f266d1c-760f-4552-ae3b-41a9b74232fa')
11+
def test_empty_basic_component(self, mock_uuid: Mock) -> None:
1012
c = Component(
1113
name='test-component', version='1.2.3'
1214
)
15+
mock_uuid.assert_called()
1316
self.assertEqual(c.name, 'test-component')
14-
self.assertEqual(c.version, '1.2.3')
1517
self.assertEqual(c.type, ComponentType.LIBRARY)
16-
self.assertEqual(len(c.external_references), 0)
17-
self.assertEqual(len(c.hashes), 0)
18+
self.assertIsNone(c.mime_type)
19+
self.assertEqual(c.bom_ref, '6f266d1c-760f-4552-ae3b-41a9b74232fa')
20+
self.assertIsNone(c.supplier)
21+
self.assertIsNone(c.author)
22+
self.assertIsNone(c.publisher)
23+
self.assertIsNone(c.group)
24+
self.assertEqual(c.version, '1.2.3')
25+
self.assertIsNone(c.description)
26+
self.assertIsNone(c.scope)
27+
self.assertListEqual(c.hashes, [])
28+
self.assertListEqual(c.licenses, [])
29+
self.assertIsNone(c.copyright)
30+
self.assertIsNone(c.purl)
31+
self.assertListEqual(c.external_references, [])
32+
self.assertIsNone(c.properties)
33+
self.assertIsNone(c.release_notes)
34+
1835
self.assertEqual(len(c.get_vulnerabilities()), 0)
1936

20-
def test_multiple_basic_components(self) -> None:
37+
@patch('cyclonedx.model.component.uuid4', return_value='6f266d1c-760f-4552-ae3b-41a9b74232fa')
38+
def test_multiple_basic_components(self, mock_uuid: Mock) -> None:
2139
c1 = Component(
2240
name='test-component', version='1.2.3'
2341
)
@@ -40,6 +58,8 @@ def test_multiple_basic_components(self) -> None:
4058

4159
self.assertNotEqual(c1, c2)
4260

61+
mock_uuid.assert_called()
62+
4363
def test_external_references(self) -> None:
4464
c = Component(
4565
name='test-component', version='1.2.3'

tests/test_model_vulnerability.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import unittest
22
from unittest import TestCase
3+
from unittest.mock import Mock, patch
34

4-
from cyclonedx.model.vulnerability import VulnerabilityRating, VulnerabilitySeverity, VulnerabilityScoreSource
5+
from cyclonedx.model.vulnerability import Vulnerability, VulnerabilityRating, VulnerabilitySeverity, \
6+
VulnerabilityScoreSource
57

68

79
class TestModelVulnerability(TestCase):
@@ -149,3 +151,25 @@ def test_v_source_get_localised_vector_other_2(self) -> None:
149151
VulnerabilityScoreSource.OTHER.get_localised_vector(vector='SOMETHING_OR_OTHER'),
150152
'SOMETHING_OR_OTHER'
151153
)
154+
155+
@patch('cyclonedx.model.vulnerability.uuid4', return_value='0afa65bc-4acd-428b-9e17-8e97b1969745')
156+
def test_empty_vulnerability(self, mock_uuid: Mock) -> None:
157+
v = Vulnerability()
158+
mock_uuid.assert_called()
159+
self.assertEqual(v.bom_ref, '0afa65bc-4acd-428b-9e17-8e97b1969745')
160+
self.assertIsNone(v.id)
161+
self.assertIsNone(v.source)
162+
self.assertListEqual(v.references, [])
163+
self.assertListEqual(v.ratings, [])
164+
self.assertListEqual(v.cwes, [])
165+
self.assertIsNone(v.description)
166+
self.assertIsNone(v.detail)
167+
self.assertIsNone(v.recommendation)
168+
self.assertListEqual(v.advisories, [])
169+
self.assertIsNone(v.created)
170+
self.assertIsNone(v.published)
171+
self.assertIsNone(v.updated)
172+
self.assertIsNone(v.credits)
173+
self.assertListEqual(v.tools, [])
174+
self.assertIsNone(v.analysis)
175+
self.assertListEqual(v.affects, [])

tests/test_output_json.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from datetime import datetime, timezone
2222
from os.path import dirname, join
2323
from packageurl import PackageURL
24+
from unittest.mock import Mock, patch
2425

2526
from cyclonedx.model import Encoding, ExternalReference, ExternalReferenceType, HashType, LicenseChoice, Note, \
2627
NoteText, OrganizationalContact, OrganizationalEntity, Property, Tool, XsUri
@@ -382,10 +383,13 @@ def test_simple_bom_v1_4_with_vulnerabilities(self) -> None:
382383
self.assertEqualJsonBom(expected_json.read(), outputter.output_as_string())
383384
expected_json.close()
384385

385-
def test_bom_v1_3_with_metadata_component(self) -> None:
386+
@patch('cyclonedx.model.component.uuid4', return_value='be2c6502-7e9a-47db-9a66-e34f729810a3')
387+
def test_bom_v1_3_with_metadata_component(self, mock_uuid: Mock) -> None:
386388
bom = Bom()
387389
bom.metadata.component = Component(
388-
name='cyclonedx-python-lib', version='1.0.0', component_type=ComponentType.LIBRARY)
390+
name='cyclonedx-python-lib', version='1.0.0', component_type=ComponentType.LIBRARY
391+
)
392+
mock_uuid.assert_called()
389393
outputter = get_instance(bom=bom, output_format=OutputFormat.JSON)
390394
self.assertIsInstance(outputter, JsonV1Dot3)
391395
with open(join(dirname(__file__), 'fixtures/bom_v1.3_with_metadata_component.json')) as expected_json:

tests/test_output_xml.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from decimal import Decimal
2222
from os.path import dirname, join
2323
from packageurl import PackageURL
24+
from unittest.mock import Mock, patch
2425

2526
from cyclonedx.model import Encoding, ExternalReference, ExternalReferenceType, HashType, Note, NoteText, \
2627
OrganizationalContact, OrganizationalEntity, Property, Tool, XsUri
@@ -520,10 +521,13 @@ def test_with_component_release_notes_post_1_4(self) -> None:
520521
namespace=outputter.get_target_namespace())
521522
expected_xml.close()
522523

523-
def test_bom_v1_3_with_metadata_component(self) -> None:
524+
@patch('cyclonedx.model.component.uuid4', return_value='5d82790b-3139-431d-855a-ab63d14a18bb')
525+
def test_bom_v1_3_with_metadata_component(self, mock_uuid: Mock) -> None:
524526
bom = Bom()
525527
bom.metadata.component = Component(
526-
name='cyclonedx-python-lib', version='1.0.0', component_type=ComponentType.LIBRARY)
528+
name='cyclonedx-python-lib', version='1.0.0', component_type=ComponentType.LIBRARY
529+
)
530+
mock_uuid.assert_called()
527531
outputter: Xml = get_instance(bom=bom)
528532
self.assertIsInstance(outputter, XmlV1Dot3)
529533
with open(join(dirname(__file__), 'fixtures/bom_v1.3_with_metadata_component.xml')) as expected_xml:

0 commit comments

Comments
 (0)