From 54cf48ffe247d1b91eb92a65e242b1b1dc9a84f4 Mon Sep 17 00:00:00 2001 From: yowgf Date: Wed, 12 Mar 2025 09:24:37 -0500 Subject: [PATCH 1/3] feat: add bom_ref to OrganizationalEntity Signed-off-by: yowgf --- cyclonedx/model/contact.py | 17 +++++++++++++ tests/test_model.py | 50 +++++++++++++++++++++++++++++++++++++- 2 files changed, 66 insertions(+), 1 deletion(-) diff --git a/cyclonedx/model/contact.py b/cyclonedx/model/contact.py index cea865e7..66790084 100644 --- a/cyclonedx/model/contact.py +++ b/cyclonedx/model/contact.py @@ -288,16 +288,33 @@ class OrganizationalEntity: def __init__( self, *, + bom_ref: Optional[Union[str, BomRef]] = None, name: Optional[str] = None, urls: Optional[Iterable[XsUri]] = None, contacts: Optional[Iterable[OrganizationalContact]] = None, address: Optional[PostalAddress] = None, ) -> None: + self._bom_ref = bom_ref self.name = name self.address = address self.urls = urls or [] # type:ignore[assignment] self.contacts = contacts or [] # type:ignore[assignment] + @property + @serializable.json_name('bom-ref') + @serializable.type_mapping(BomRef) + @serializable.xml_attribute() + @serializable.xml_name('bom-ref') + def bom_ref(self) -> Optional[BomRef]: + """ + An optional identifier which can be used to reference the component elsewhere in the BOM. Every bom-ref MUST be + unique within the BOM. + + Returns: + `BomRef` + """ + return self._bom_ref + @property @serializable.xml_sequence(10) @serializable.xml_string(serializable.XmlStringSerializationType.NORMALIZED_STRING) diff --git a/tests/test_model.py b/tests/test_model.py index 58c017e9..0110fe68 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -22,6 +22,7 @@ from uuid import UUID from ddt import ddt, named_data +from sortedcontainers import SortedSet from cyclonedx._internal.compare import ComparableTuple from cyclonedx.exception.model import InvalidLocaleTypeException, InvalidUriException, UnknownHashTypeException @@ -39,7 +40,7 @@ XsUri, ) from cyclonedx.model.bom_ref import BomRef -from cyclonedx.model.contact import OrganizationalContact +from cyclonedx.model.contact import OrganizationalContact, OrganizationalEntity, PostalAddress from cyclonedx.model.issue import IssueClassification, IssueType, IssueTypeSource from tests import reorder @@ -463,6 +464,53 @@ def test_sort(self) -> None: self.assertListEqual(sorted_contacts, expected_contacts) +class TestModelOrganizationalEntity(TestCase): + def test_init_with_bom_ref(self) -> None: + contacts = [ + OrganizationalContact(name='a', email='a', phone='a'), + OrganizationalContact(name='b', email='a', phone='a'), + ] + OrganizationalEntity( + bom_ref=BomRef('dummy-bom-ref'), + name='dummy-organizational-entity', + contacts=contacts, + address=PostalAddress( + country='dummy-country', + region='dummy-region', + ), + ) + + def test_init_without_bom_ref(self) -> None: + contacts = [ + OrganizationalContact(name='a', email='a', phone='a'), + OrganizationalContact(name='b', email='a', phone='a'), + ] + OrganizationalEntity( + name='dummy-organizational-entity', + contacts=contacts, + address=PostalAddress( + country='dummy-country', + region='dummy-region', + ), + ) + + def test_init_from_json(self) -> None: + bom_ref = 'Example' + name = 'Example' + urls = [ + 'https://example.com/' + ] + specification = { + 'name': name, + 'url': urls, + 'bom-ref': bom_ref + } + entity = OrganizationalEntity.from_json(specification) + assert entity.name == name + assert entity.urls == SortedSet([XsUri(url) for url in urls]) + assert entity.bom_ref == BomRef(bom_ref) + + class TestModelXsUri(TestCase): # URI samples taken from http://www.datypic.com/sc/xsd/t-xsd_anyURI.html From 3c9f234aa242727934fbfefbc8df305c90db2ad9 Mon Sep 17 00:00:00 2001 From: yowgf Date: Wed, 12 Mar 2025 09:33:10 -0500 Subject: [PATCH 2/3] Address concerns from gemini bot Signed-off-by: yowgf --- tests/test_model.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/test_model.py b/tests/test_model.py index 0110fe68..9e00de53 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -509,6 +509,20 @@ def test_init_from_json(self) -> None: assert entity.name == name assert entity.urls == SortedSet([XsUri(url) for url in urls]) assert entity.bom_ref == BomRef(bom_ref) + assert len(entity.urls) == 1 + assert next(iter(entity.urls)) == XsUri(urls[0]) + + def test_init_from_json_without_url(self) -> None: + bom_ref = 'Example' + name = 'Example' + specification = { + 'name': name, + 'bom-ref': bom_ref + } + entity = OrganizationalEntity.from_json(specification) + assert entity.name == name + assert entity.urls == SortedSet() + assert entity.bom_ref == BomRef(bom_ref) class TestModelXsUri(TestCase): From 640a90411edffb76847a8291d02c12ff14254e53 Mon Sep 17 00:00:00 2001 From: yowgf Date: Thu, 13 Mar 2025 09:59:12 -0500 Subject: [PATCH 3/3] Add tests in tests/_data/models.py, remove current Signed-off-by: yowgf --- tests/_data/models.py | 20 ++++++ ..._organizational_entity_bom_ref-1.0.xml.bin | 4 ++ ..._organizational_entity_bom_ref-1.1.xml.bin | 4 ++ ...organizational_entity_bom_ref-1.2.json.bin | 15 +++++ ..._organizational_entity_bom_ref-1.2.xml.bin | 11 ++++ ...organizational_entity_bom_ref-1.3.json.bin | 15 +++++ ..._organizational_entity_bom_ref-1.3.xml.bin | 11 ++++ ...organizational_entity_bom_ref-1.4.json.bin | 15 +++++ ..._organizational_entity_bom_ref-1.4.xml.bin | 11 ++++ ...organizational_entity_bom_ref-1.5.json.bin | 31 +++++++++ ..._organizational_entity_bom_ref-1.5.xml.bin | 20 ++++++ ...organizational_entity_bom_ref-1.6.json.bin | 31 +++++++++ ..._organizational_entity_bom_ref-1.6.xml.bin | 20 ++++++ tests/test_model.py | 64 +------------------ 14 files changed, 209 insertions(+), 63 deletions(-) create mode 100644 tests/_data/snapshots/get_bom_for_issue799_organizational_entity_bom_ref-1.0.xml.bin create mode 100644 tests/_data/snapshots/get_bom_for_issue799_organizational_entity_bom_ref-1.1.xml.bin create mode 100644 tests/_data/snapshots/get_bom_for_issue799_organizational_entity_bom_ref-1.2.json.bin create mode 100644 tests/_data/snapshots/get_bom_for_issue799_organizational_entity_bom_ref-1.2.xml.bin create mode 100644 tests/_data/snapshots/get_bom_for_issue799_organizational_entity_bom_ref-1.3.json.bin create mode 100644 tests/_data/snapshots/get_bom_for_issue799_organizational_entity_bom_ref-1.3.xml.bin create mode 100644 tests/_data/snapshots/get_bom_for_issue799_organizational_entity_bom_ref-1.4.json.bin create mode 100644 tests/_data/snapshots/get_bom_for_issue799_organizational_entity_bom_ref-1.4.xml.bin create mode 100644 tests/_data/snapshots/get_bom_for_issue799_organizational_entity_bom_ref-1.5.json.bin create mode 100644 tests/_data/snapshots/get_bom_for_issue799_organizational_entity_bom_ref-1.5.xml.bin create mode 100644 tests/_data/snapshots/get_bom_for_issue799_organizational_entity_bom_ref-1.6.json.bin create mode 100644 tests/_data/snapshots/get_bom_for_issue799_organizational_entity_bom_ref-1.6.xml.bin diff --git a/tests/_data/models.py b/tests/_data/models.py index 6a25c552..bba0fc87 100644 --- a/tests/_data/models.py +++ b/tests/_data/models.py @@ -1437,6 +1437,26 @@ def get_bom_for_issue540_duplicate_components() -> Bom: bom.register_dependency(component1, [component3]) return bom + +def get_bom_for_issue799_organizational_entity_bom_ref() -> Bom: + """regression test for issue #799 + see https://github.com/CycloneDX/cyclonedx-python-lib/issues/799 + """ + return _make_bom( + metadata=BomMetaData( + tools=ToolRepository( + services=( + Service(name='service-1', + provider=OrganizationalEntity( + name='org-1', + bom_ref=BomRef('bom-ref-1'), + )), + ) + ) + ) + ) + + # --- diff --git a/tests/_data/snapshots/get_bom_for_issue799_organizational_entity_bom_ref-1.0.xml.bin b/tests/_data/snapshots/get_bom_for_issue799_organizational_entity_bom_ref-1.0.xml.bin new file mode 100644 index 00000000..acb06612 --- /dev/null +++ b/tests/_data/snapshots/get_bom_for_issue799_organizational_entity_bom_ref-1.0.xml.bin @@ -0,0 +1,4 @@ + + + + diff --git a/tests/_data/snapshots/get_bom_for_issue799_organizational_entity_bom_ref-1.1.xml.bin b/tests/_data/snapshots/get_bom_for_issue799_organizational_entity_bom_ref-1.1.xml.bin new file mode 100644 index 00000000..55ef5cda --- /dev/null +++ b/tests/_data/snapshots/get_bom_for_issue799_organizational_entity_bom_ref-1.1.xml.bin @@ -0,0 +1,4 @@ + + + + diff --git a/tests/_data/snapshots/get_bom_for_issue799_organizational_entity_bom_ref-1.2.json.bin b/tests/_data/snapshots/get_bom_for_issue799_organizational_entity_bom_ref-1.2.json.bin new file mode 100644 index 00000000..2a9b4212 --- /dev/null +++ b/tests/_data/snapshots/get_bom_for_issue799_organizational_entity_bom_ref-1.2.json.bin @@ -0,0 +1,15 @@ +{ + "metadata": { + "timestamp": "2023-01-07T13:44:32.312678+00:00", + "tools": [ + { + "name": "service-1" + } + ] + }, + "serialNumber": "urn:uuid:1441d33a-e0fc-45b5-af3b-61ee52a88bac", + "version": 1, + "$schema": "http://cyclonedx.org/schema/bom-1.2b.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.2" +} \ No newline at end of file diff --git a/tests/_data/snapshots/get_bom_for_issue799_organizational_entity_bom_ref-1.2.xml.bin b/tests/_data/snapshots/get_bom_for_issue799_organizational_entity_bom_ref-1.2.xml.bin new file mode 100644 index 00000000..0551f582 --- /dev/null +++ b/tests/_data/snapshots/get_bom_for_issue799_organizational_entity_bom_ref-1.2.xml.bin @@ -0,0 +1,11 @@ + + + + 2023-01-07T13:44:32.312678+00:00 + + + service-1 + + + + diff --git a/tests/_data/snapshots/get_bom_for_issue799_organizational_entity_bom_ref-1.3.json.bin b/tests/_data/snapshots/get_bom_for_issue799_organizational_entity_bom_ref-1.3.json.bin new file mode 100644 index 00000000..0f3f0723 --- /dev/null +++ b/tests/_data/snapshots/get_bom_for_issue799_organizational_entity_bom_ref-1.3.json.bin @@ -0,0 +1,15 @@ +{ + "metadata": { + "timestamp": "2023-01-07T13:44:32.312678+00:00", + "tools": [ + { + "name": "service-1" + } + ] + }, + "serialNumber": "urn:uuid:1441d33a-e0fc-45b5-af3b-61ee52a88bac", + "version": 1, + "$schema": "http://cyclonedx.org/schema/bom-1.3a.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.3" +} \ No newline at end of file diff --git a/tests/_data/snapshots/get_bom_for_issue799_organizational_entity_bom_ref-1.3.xml.bin b/tests/_data/snapshots/get_bom_for_issue799_organizational_entity_bom_ref-1.3.xml.bin new file mode 100644 index 00000000..7d959fb2 --- /dev/null +++ b/tests/_data/snapshots/get_bom_for_issue799_organizational_entity_bom_ref-1.3.xml.bin @@ -0,0 +1,11 @@ + + + + 2023-01-07T13:44:32.312678+00:00 + + + service-1 + + + + diff --git a/tests/_data/snapshots/get_bom_for_issue799_organizational_entity_bom_ref-1.4.json.bin b/tests/_data/snapshots/get_bom_for_issue799_organizational_entity_bom_ref-1.4.json.bin new file mode 100644 index 00000000..eef216da --- /dev/null +++ b/tests/_data/snapshots/get_bom_for_issue799_organizational_entity_bom_ref-1.4.json.bin @@ -0,0 +1,15 @@ +{ + "metadata": { + "timestamp": "2023-01-07T13:44:32.312678+00:00", + "tools": [ + { + "name": "service-1" + } + ] + }, + "serialNumber": "urn:uuid:1441d33a-e0fc-45b5-af3b-61ee52a88bac", + "version": 1, + "$schema": "http://cyclonedx.org/schema/bom-1.4.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.4" +} \ No newline at end of file diff --git a/tests/_data/snapshots/get_bom_for_issue799_organizational_entity_bom_ref-1.4.xml.bin b/tests/_data/snapshots/get_bom_for_issue799_organizational_entity_bom_ref-1.4.xml.bin new file mode 100644 index 00000000..e7e377e2 --- /dev/null +++ b/tests/_data/snapshots/get_bom_for_issue799_organizational_entity_bom_ref-1.4.xml.bin @@ -0,0 +1,11 @@ + + + + 2023-01-07T13:44:32.312678+00:00 + + + service-1 + + + + diff --git a/tests/_data/snapshots/get_bom_for_issue799_organizational_entity_bom_ref-1.5.json.bin b/tests/_data/snapshots/get_bom_for_issue799_organizational_entity_bom_ref-1.5.json.bin new file mode 100644 index 00000000..bf352045 --- /dev/null +++ b/tests/_data/snapshots/get_bom_for_issue799_organizational_entity_bom_ref-1.5.json.bin @@ -0,0 +1,31 @@ +{ + "metadata": { + "timestamp": "2023-01-07T13:44:32.312678+00:00", + "tools": { + "services": [ + { + "name": "service-1", + "provider": { + "bom-ref": "bom-ref-1", + "name": "org-1" + } + } + ] + } + }, + "properties": [ + { + "name": "key1", + "value": "val1" + }, + { + "name": "key2", + "value": "val2" + } + ], + "serialNumber": "urn:uuid:1441d33a-e0fc-45b5-af3b-61ee52a88bac", + "version": 1, + "$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.5" +} \ No newline at end of file diff --git a/tests/_data/snapshots/get_bom_for_issue799_organizational_entity_bom_ref-1.5.xml.bin b/tests/_data/snapshots/get_bom_for_issue799_organizational_entity_bom_ref-1.5.xml.bin new file mode 100644 index 00000000..d3656fac --- /dev/null +++ b/tests/_data/snapshots/get_bom_for_issue799_organizational_entity_bom_ref-1.5.xml.bin @@ -0,0 +1,20 @@ + + + + 2023-01-07T13:44:32.312678+00:00 + + + + + org-1 + + service-1 + + + + + + val1 + val2 + + diff --git a/tests/_data/snapshots/get_bom_for_issue799_organizational_entity_bom_ref-1.6.json.bin b/tests/_data/snapshots/get_bom_for_issue799_organizational_entity_bom_ref-1.6.json.bin new file mode 100644 index 00000000..148f49dd --- /dev/null +++ b/tests/_data/snapshots/get_bom_for_issue799_organizational_entity_bom_ref-1.6.json.bin @@ -0,0 +1,31 @@ +{ + "metadata": { + "timestamp": "2023-01-07T13:44:32.312678+00:00", + "tools": { + "services": [ + { + "name": "service-1", + "provider": { + "bom-ref": "bom-ref-1", + "name": "org-1" + } + } + ] + } + }, + "properties": [ + { + "name": "key1", + "value": "val1" + }, + { + "name": "key2", + "value": "val2" + } + ], + "serialNumber": "urn:uuid:1441d33a-e0fc-45b5-af3b-61ee52a88bac", + "version": 1, + "$schema": "http://cyclonedx.org/schema/bom-1.6.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.6" +} \ No newline at end of file diff --git a/tests/_data/snapshots/get_bom_for_issue799_organizational_entity_bom_ref-1.6.xml.bin b/tests/_data/snapshots/get_bom_for_issue799_organizational_entity_bom_ref-1.6.xml.bin new file mode 100644 index 00000000..0e695bf2 --- /dev/null +++ b/tests/_data/snapshots/get_bom_for_issue799_organizational_entity_bom_ref-1.6.xml.bin @@ -0,0 +1,20 @@ + + + + 2023-01-07T13:44:32.312678+00:00 + + + + + org-1 + + service-1 + + + + + + val1 + val2 + + diff --git a/tests/test_model.py b/tests/test_model.py index 9e00de53..58c017e9 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -22,7 +22,6 @@ from uuid import UUID from ddt import ddt, named_data -from sortedcontainers import SortedSet from cyclonedx._internal.compare import ComparableTuple from cyclonedx.exception.model import InvalidLocaleTypeException, InvalidUriException, UnknownHashTypeException @@ -40,7 +39,7 @@ XsUri, ) from cyclonedx.model.bom_ref import BomRef -from cyclonedx.model.contact import OrganizationalContact, OrganizationalEntity, PostalAddress +from cyclonedx.model.contact import OrganizationalContact from cyclonedx.model.issue import IssueClassification, IssueType, IssueTypeSource from tests import reorder @@ -464,67 +463,6 @@ def test_sort(self) -> None: self.assertListEqual(sorted_contacts, expected_contacts) -class TestModelOrganizationalEntity(TestCase): - def test_init_with_bom_ref(self) -> None: - contacts = [ - OrganizationalContact(name='a', email='a', phone='a'), - OrganizationalContact(name='b', email='a', phone='a'), - ] - OrganizationalEntity( - bom_ref=BomRef('dummy-bom-ref'), - name='dummy-organizational-entity', - contacts=contacts, - address=PostalAddress( - country='dummy-country', - region='dummy-region', - ), - ) - - def test_init_without_bom_ref(self) -> None: - contacts = [ - OrganizationalContact(name='a', email='a', phone='a'), - OrganizationalContact(name='b', email='a', phone='a'), - ] - OrganizationalEntity( - name='dummy-organizational-entity', - contacts=contacts, - address=PostalAddress( - country='dummy-country', - region='dummy-region', - ), - ) - - def test_init_from_json(self) -> None: - bom_ref = 'Example' - name = 'Example' - urls = [ - 'https://example.com/' - ] - specification = { - 'name': name, - 'url': urls, - 'bom-ref': bom_ref - } - entity = OrganizationalEntity.from_json(specification) - assert entity.name == name - assert entity.urls == SortedSet([XsUri(url) for url in urls]) - assert entity.bom_ref == BomRef(bom_ref) - assert len(entity.urls) == 1 - assert next(iter(entity.urls)) == XsUri(urls[0]) - - def test_init_from_json_without_url(self) -> None: - bom_ref = 'Example' - name = 'Example' - specification = { - 'name': name, - 'bom-ref': bom_ref - } - entity = OrganizationalEntity.from_json(specification) - assert entity.name == name - assert entity.urls == SortedSet() - assert entity.bom_ref == BomRef(bom_ref) - - class TestModelXsUri(TestCase): # URI samples taken from http://www.datypic.com/sc/xsd/t-xsd_anyURI.html