Skip to content

Commit 5ec73d0

Browse files
saquibsaifeeSaquib Saifee
and
Saquib Saifee
authored
feat: add factory method XsUri.make_bom_link() (#728)
--------- Signed-off-by: Saquib Saifee <[email protected]> Co-authored-by: Saquib Saifee <[email protected]>
1 parent 5860b67 commit 5ec73d0

File tree

3 files changed

+53
-3
lines changed

3 files changed

+53
-3
lines changed

cyclonedx/model/__init__.py

+36-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@
2727
from enum import Enum
2828
from functools import reduce
2929
from json import loads as json_loads
30-
from typing import Any, Dict, FrozenSet, Generator, Iterable, List, Optional, Tuple, Type
30+
from typing import Any, Dict, FrozenSet, Generator, Iterable, List, Optional, Tuple, Type, Union
31+
from urllib.parse import quote as url_quote
32+
from uuid import UUID
3133
from warnings import warn
3234
from xml.etree.ElementTree import Element as XmlElement # nosec B405
3335

@@ -51,6 +53,9 @@
5153
SchemaVersion1Dot5,
5254
SchemaVersion1Dot6,
5355
)
56+
from .bom_ref import BomRef
57+
58+
_BOM_LINK_PREFIX = 'urn:cdx:'
5459

5560

5661
@serializable.serializable_enum
@@ -767,6 +772,36 @@ def deserialize(cls, o: Any) -> 'XsUri':
767772
f'XsUri string supplied does not parse: {o!r}'
768773
) from err
769774

775+
@classmethod
776+
def make_bom_link(
777+
cls,
778+
serial_number: Union[UUID, str],
779+
version: int = 1,
780+
bom_ref: Optional[Union[str, BomRef]] = None
781+
) -> 'XsUri':
782+
"""
783+
Generate a BOM-Link URI.
784+
785+
Args:
786+
serial_number: The unique serial number of the BOM.
787+
version: The version of the BOM. The default version is 1.
788+
bom_ref: The unique identifier of the component, service, or vulnerability within the BOM.
789+
790+
Returns:
791+
XsUri: Instance of XsUri with the generated BOM-Link URI.
792+
"""
793+
bom_ref_part = f'#{url_quote(str(bom_ref))}' if bom_ref else ''
794+
return cls(f'{_BOM_LINK_PREFIX}{serial_number}/{version}{bom_ref_part}')
795+
796+
def is_bom_link(self) -> bool:
797+
"""
798+
Check if the URI is a BOM-Link.
799+
800+
Returns:
801+
`bool`
802+
"""
803+
return self._uri.startswith(_BOM_LINK_PREFIX)
804+
770805

771806
@serializable.serializable_class
772807
class ExternalReference:

cyclonedx/model/bom.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
SchemaVersion1Dot6,
3838
)
3939
from ..serialization import LicenseRepositoryHelper, UrnUuidHelper
40-
from . import ExternalReference, Property
40+
from . import _BOM_LINK_PREFIX, ExternalReference, Property
4141
from .bom_ref import BomRef
4242
from .component import Component
4343
from .contact import OrganizationalContact, OrganizationalEntity
@@ -663,7 +663,7 @@ def register_dependency(self, target: Dependable, depends_on: Optional[Iterable[
663663
self.register_dependency(target=_d2, depends_on=None)
664664

665665
def urn(self) -> str:
666-
return f'urn:cdx:{self.serial_number}/{self.version}'
666+
return f'{_BOM_LINK_PREFIX}{self.serial_number}/{self.version}'
667667

668668
def validate(self) -> bool:
669669
"""

tests/test_model.py

+15
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import datetime
2020
from enum import Enum
2121
from unittest import TestCase
22+
from uuid import UUID
2223

2324
from ddt import ddt, named_data
2425

@@ -42,6 +43,7 @@
4243
Property,
4344
XsUri,
4445
)
46+
from cyclonedx.model.bom_ref import BomRef
4547
from cyclonedx.model.contact import OrganizationalContact
4648
from cyclonedx.model.issue import IssueClassification, IssueType, IssueTypeSource
4749
from tests import reorder
@@ -545,6 +547,19 @@ def test_sort(self) -> None:
545547
expected_uris = reorder(uris, expected_order)
546548
self.assertListEqual(sorted_uris, expected_uris)
547549

550+
def test_make_bom_link_without_bom_ref(self) -> None:
551+
bom_link = XsUri.make_bom_link(UUID('e5a93409-fd7c-4ffa-bf7f-6dc1630b1b9d'), 2)
552+
self.assertEqual(bom_link.uri, 'urn:cdx:e5a93409-fd7c-4ffa-bf7f-6dc1630b1b9d/2')
553+
554+
def test_make_bom_link_with_bom_ref(self) -> None:
555+
bom_link = XsUri.make_bom_link(UUID('e5a93409-fd7c-4ffa-bf7f-6dc1630b1b9d'),
556+
2, BomRef('componentA#sub-componentB%2'))
557+
self.assertEqual(bom_link.uri, 'urn:cdx:e5a93409-fd7c-4ffa-bf7f-6dc1630b1b9d/2#componentA%23sub-componentB%252')
558+
559+
def test_is_bom_link(self) -> None:
560+
self.assertTrue(XsUri('urn:cdx:e5a93409-fd7c-4ffa-bf7f-6dc1630b1b9d/2').is_bom_link())
561+
self.assertFalse(XsUri('http://example.com/resource').is_bom_link())
562+
548563

549564
class TestModelProperty(TestCase):
550565

0 commit comments

Comments
 (0)