diff --git a/cyclonedx/model/component.py b/cyclonedx/model/component.py
index f0b1e6ad..8998620c 100644
--- a/cyclonedx/model/component.py
+++ b/cyclonedx/model/component.py
@@ -21,6 +21,7 @@
from enum import Enum
from os.path import exists
from typing import List, Optional
+from uuid import uuid4
# See https://github.com/package-url/packageurl-python/issues/65
from packageurl import PackageURL # type: ignore
@@ -112,7 +113,7 @@ def __init__(self, name: str, component_type: ComponentType = ComponentType.LIBR
) -> None:
self.type = component_type
self.mime_type = mime_type
- self.bom_ref = bom_ref
+ self.bom_ref = bom_ref or str(uuid4())
self.supplier = supplier
self.author = author
self.publisher = publisher
@@ -189,8 +190,10 @@ def bom_ref(self) -> Optional[str]:
An optional identifier which can be used to reference the component elsewhere in the BOM. Every bom-ref MUST be
unique within the BOM.
+ If a value was not provided in the constructor, a UUIDv4 will have been assigned.
+
Returns:
- `str` as a unique identifiers for this Component if set else `None`
+ `str` as a unique identifiers for this Component
"""
return self._bom_ref
@@ -507,7 +510,7 @@ def __eq__(self, other: object) -> bool:
def __hash__(self) -> int:
return hash((
- self.author, self.bom_ref, self.copyright, self.description, str(self.external_references), self.group,
+ self.author, self.copyright, self.description, str(self.external_references), self.group,
str(self.hashes), str(self.licenses), self.mime_type, self.name, self.properties, self.publisher, self.purl,
self.release_notes, self.scope, self.supplier, self.type, self.version, self.cpe
))
diff --git a/cyclonedx/model/vulnerability.py b/cyclonedx/model/vulnerability.py
index 1e7e832f..1a7a9720 100644
--- a/cyclonedx/model/vulnerability.py
+++ b/cyclonedx/model/vulnerability.py
@@ -24,6 +24,7 @@
from enum import Enum
from typing import List, Optional, Tuple, Union
from urllib.parse import ParseResult, urlparse
+from uuid import uuid4
from . import OrganizationalContact, OrganizationalEntity, Tool, XsUri
from .impact_analysis import ImpactAnalysisAffectedStatus, ImpactAnalysisJustification, ImpactAnalysisResponse, \
@@ -644,7 +645,7 @@ def __init__(self, bom_ref: Optional[str] = None, id: Optional[str] = None,
# Deprecated Parameters kept for backwards compatibility
source_name: Optional[str] = None, source_url: Optional[str] = None,
recommendations: Optional[List[str]] = None) -> None:
- self.bom_ref = bom_ref
+ self.bom_ref = bom_ref or str(uuid4())
self.id = id
self.source = source
self.references = references or []
@@ -677,8 +678,10 @@ def bom_ref(self) -> Optional[str]:
"""
Get the unique reference for this Vulnerability in this BOM.
+ If a value was not provided in the constructor, a UUIDv4 will have been assigned.
+
Returns:
- `str` if set else `None`
+ `str` unique identifier for this Vulnerability
"""
return self._bom_ref
diff --git a/docs/modelling.rst b/docs/modelling.rst
index f8c36cc3..68626f4b 100644
--- a/docs/modelling.rst
+++ b/docs/modelling.rst
@@ -15,13 +15,17 @@ Examples
From a Parser
~~~~~~~~~~~~~
+ **Note:** Concreate parser implementations were moved out of this library and into `CycloneDX Python`_ as of version
+ ``1.0.0``.
+
.. code-block:: python
from cyclonedx.model.bom import Bom
- from cyclonedx.parser.environment import EnvironmentParser
+ from cyclonedx_py.parser.environment import EnvironmentParser
parser = EnvironmentParser()
bom = Bom.from_parser(parser=parser)
+.. _CycloneDX Python: https://github.com/CycloneDX/cyclonedx-python
.. _Jake: https://pypi.org/project/jake
\ No newline at end of file
diff --git a/tests/fixtures/bom_v1.3_with_metadata_component.json b/tests/fixtures/bom_v1.3_with_metadata_component.json
index 1cb8628a..7290dfc7 100644
--- a/tests/fixtures/bom_v1.3_with_metadata_component.json
+++ b/tests/fixtures/bom_v1.3_with_metadata_component.json
@@ -14,6 +14,7 @@
}
],
"component": {
+ "bom-ref": "be2c6502-7e9a-47db-9a66-e34f729810a3",
"type": "library",
"name": "cyclonedx-python-lib",
"version": "1.0.0"
diff --git a/tests/fixtures/bom_v1.3_with_metadata_component.xml b/tests/fixtures/bom_v1.3_with_metadata_component.xml
index 6baf1884..1bbe3362 100644
--- a/tests/fixtures/bom_v1.3_with_metadata_component.xml
+++ b/tests/fixtures/bom_v1.3_with_metadata_component.xml
@@ -9,7 +9,7 @@
VERSION
-
+
cyclonedx-python-lib
1.0.0
diff --git a/tests/test_model_component.py b/tests/test_model_component.py
index ee8b42a6..a150b39a 100644
--- a/tests/test_model_component.py
+++ b/tests/test_model_component.py
@@ -1,4 +1,5 @@
from unittest import TestCase
+from unittest.mock import Mock, patch
from cyclonedx.model import ExternalReference, ExternalReferenceType
from cyclonedx.model.component import Component, ComponentType
@@ -6,18 +7,35 @@
class TestModelComponent(TestCase):
- def test_empty_basic_component(self) -> None:
+ @patch('cyclonedx.model.component.uuid4', return_value='6f266d1c-760f-4552-ae3b-41a9b74232fa')
+ def test_empty_basic_component(self, mock_uuid: Mock) -> None:
c = Component(
name='test-component', version='1.2.3'
)
+ mock_uuid.assert_called()
self.assertEqual(c.name, 'test-component')
- self.assertEqual(c.version, '1.2.3')
self.assertEqual(c.type, ComponentType.LIBRARY)
- self.assertEqual(len(c.external_references), 0)
- self.assertEqual(len(c.hashes), 0)
+ self.assertIsNone(c.mime_type)
+ self.assertEqual(c.bom_ref, '6f266d1c-760f-4552-ae3b-41a9b74232fa')
+ self.assertIsNone(c.supplier)
+ self.assertIsNone(c.author)
+ self.assertIsNone(c.publisher)
+ self.assertIsNone(c.group)
+ self.assertEqual(c.version, '1.2.3')
+ self.assertIsNone(c.description)
+ self.assertIsNone(c.scope)
+ self.assertListEqual(c.hashes, [])
+ self.assertListEqual(c.licenses, [])
+ self.assertIsNone(c.copyright)
+ self.assertIsNone(c.purl)
+ self.assertListEqual(c.external_references, [])
+ self.assertIsNone(c.properties)
+ self.assertIsNone(c.release_notes)
+
self.assertEqual(len(c.get_vulnerabilities()), 0)
- def test_multiple_basic_components(self) -> None:
+ @patch('cyclonedx.model.component.uuid4', return_value='6f266d1c-760f-4552-ae3b-41a9b74232fa')
+ def test_multiple_basic_components(self, mock_uuid: Mock) -> None:
c1 = Component(
name='test-component', version='1.2.3'
)
@@ -40,6 +58,8 @@ def test_multiple_basic_components(self) -> None:
self.assertNotEqual(c1, c2)
+ mock_uuid.assert_called()
+
def test_external_references(self) -> None:
c = Component(
name='test-component', version='1.2.3'
diff --git a/tests/test_model_vulnerability.py b/tests/test_model_vulnerability.py
index 5e7c4e22..fe302c43 100644
--- a/tests/test_model_vulnerability.py
+++ b/tests/test_model_vulnerability.py
@@ -1,7 +1,9 @@
import unittest
from unittest import TestCase
+from unittest.mock import Mock, patch
-from cyclonedx.model.vulnerability import VulnerabilityRating, VulnerabilitySeverity, VulnerabilityScoreSource
+from cyclonedx.model.vulnerability import Vulnerability, VulnerabilityRating, VulnerabilitySeverity, \
+ VulnerabilityScoreSource
class TestModelVulnerability(TestCase):
@@ -149,3 +151,25 @@ def test_v_source_get_localised_vector_other_2(self) -> None:
VulnerabilityScoreSource.OTHER.get_localised_vector(vector='SOMETHING_OR_OTHER'),
'SOMETHING_OR_OTHER'
)
+
+ @patch('cyclonedx.model.vulnerability.uuid4', return_value='0afa65bc-4acd-428b-9e17-8e97b1969745')
+ def test_empty_vulnerability(self, mock_uuid: Mock) -> None:
+ v = Vulnerability()
+ mock_uuid.assert_called()
+ self.assertEqual(v.bom_ref, '0afa65bc-4acd-428b-9e17-8e97b1969745')
+ self.assertIsNone(v.id)
+ self.assertIsNone(v.source)
+ self.assertListEqual(v.references, [])
+ self.assertListEqual(v.ratings, [])
+ self.assertListEqual(v.cwes, [])
+ self.assertIsNone(v.description)
+ self.assertIsNone(v.detail)
+ self.assertIsNone(v.recommendation)
+ self.assertListEqual(v.advisories, [])
+ self.assertIsNone(v.created)
+ self.assertIsNone(v.published)
+ self.assertIsNone(v.updated)
+ self.assertIsNone(v.credits)
+ self.assertListEqual(v.tools, [])
+ self.assertIsNone(v.analysis)
+ self.assertListEqual(v.affects, [])
diff --git a/tests/test_output_json.py b/tests/test_output_json.py
index cd043f38..c424e2f5 100644
--- a/tests/test_output_json.py
+++ b/tests/test_output_json.py
@@ -21,6 +21,7 @@
from datetime import datetime, timezone
from os.path import dirname, join
from packageurl import PackageURL
+from unittest.mock import Mock, patch
from cyclonedx.model import Encoding, ExternalReference, ExternalReferenceType, HashType, LicenseChoice, Note, \
NoteText, OrganizationalContact, OrganizationalEntity, Property, Tool, XsUri
@@ -382,10 +383,13 @@ def test_simple_bom_v1_4_with_vulnerabilities(self) -> None:
self.assertEqualJsonBom(expected_json.read(), outputter.output_as_string())
expected_json.close()
- def test_bom_v1_3_with_metadata_component(self) -> None:
+ @patch('cyclonedx.model.component.uuid4', return_value='be2c6502-7e9a-47db-9a66-e34f729810a3')
+ def test_bom_v1_3_with_metadata_component(self, mock_uuid: Mock) -> None:
bom = Bom()
bom.metadata.component = Component(
- name='cyclonedx-python-lib', version='1.0.0', component_type=ComponentType.LIBRARY)
+ name='cyclonedx-python-lib', version='1.0.0', component_type=ComponentType.LIBRARY
+ )
+ mock_uuid.assert_called()
outputter = get_instance(bom=bom, output_format=OutputFormat.JSON)
self.assertIsInstance(outputter, JsonV1Dot3)
with open(join(dirname(__file__), 'fixtures/bom_v1.3_with_metadata_component.json')) as expected_json:
diff --git a/tests/test_output_xml.py b/tests/test_output_xml.py
index 087b1416..c659d425 100644
--- a/tests/test_output_xml.py
+++ b/tests/test_output_xml.py
@@ -21,6 +21,7 @@
from decimal import Decimal
from os.path import dirname, join
from packageurl import PackageURL
+from unittest.mock import Mock, patch
from cyclonedx.model import Encoding, ExternalReference, ExternalReferenceType, HashType, Note, NoteText, \
OrganizationalContact, OrganizationalEntity, Property, Tool, XsUri
@@ -520,10 +521,13 @@ def test_with_component_release_notes_post_1_4(self) -> None:
namespace=outputter.get_target_namespace())
expected_xml.close()
- def test_bom_v1_3_with_metadata_component(self) -> None:
+ @patch('cyclonedx.model.component.uuid4', return_value='5d82790b-3139-431d-855a-ab63d14a18bb')
+ def test_bom_v1_3_with_metadata_component(self, mock_uuid: Mock) -> None:
bom = Bom()
bom.metadata.component = Component(
- name='cyclonedx-python-lib', version='1.0.0', component_type=ComponentType.LIBRARY)
+ name='cyclonedx-python-lib', version='1.0.0', component_type=ComponentType.LIBRARY
+ )
+ mock_uuid.assert_called()
outputter: Xml = get_instance(bom=bom)
self.assertIsInstance(outputter, XmlV1Dot3)
with open(join(dirname(__file__), 'fixtures/bom_v1.3_with_metadata_component.xml')) as expected_xml: