Skip to content

feat: bom-ref for Compomnent and Vulnerability default to a UUID #142

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jan 24, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -111,7 +112,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 @@ -187,8 +188,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 @@ -490,7 +493,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
))
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 @@ -328,10 +329,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 @@ -434,10 +435,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