Skip to content

Commit

Permalink
feat: bom-ref for Component and Vulnerability default to a UUID (#142)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
madpah authored Jan 24, 2022
1 parent 97c215c commit 3953bb6
Show file tree
Hide file tree
Showing 9 changed files with 80 additions and 17 deletions.
9 changes: 6 additions & 3 deletions cyclonedx/model/component.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
))
Expand Down
7 changes: 5 additions & 2 deletions cyclonedx/model/vulnerability.py
Original file line number Diff line number Diff line change
Expand Up @@ -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, \
Expand Down Expand Up @@ -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 []
Expand Down Expand Up @@ -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

Expand Down
6 changes: 5 additions & 1 deletion docs/modelling.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
1 change: 1 addition & 0 deletions tests/fixtures/bom_v1.3_with_metadata_component.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
}
],
"component": {
"bom-ref": "be2c6502-7e9a-47db-9a66-e34f729810a3",
"type": "library",
"name": "cyclonedx-python-lib",
"version": "1.0.0"
Expand Down
2 changes: 1 addition & 1 deletion tests/fixtures/bom_v1.3_with_metadata_component.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<version>VERSION</version>
</tool>
</tools>
<component type="library">
<component type="library" bom-ref="5d82790b-3139-431d-855a-ab63d14a18bb">
<name>cyclonedx-python-lib</name>
<version>1.0.0</version>
</component>
Expand Down
30 changes: 25 additions & 5 deletions tests/test_model_component.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,41 @@
from unittest import TestCase
from unittest.mock import Mock, patch

from cyclonedx.model import ExternalReference, ExternalReferenceType
from cyclonedx.model.component import Component, ComponentType


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'
)
Expand All @@ -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'
Expand Down
26 changes: 25 additions & 1 deletion tests/test_model_vulnerability.py
Original file line number Diff line number Diff line change
@@ -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):
Expand Down Expand Up @@ -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, [])
8 changes: 6 additions & 2 deletions tests/test_output_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
8 changes: 6 additions & 2 deletions tests/test_output_xml.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down

0 comments on commit 3953bb6

Please sign in to comment.