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: