diff --git a/cyclonedx/model/component.py b/cyclonedx/model/component.py index 89e7020d..d3b27201 100644 --- a/cyclonedx/model/component.py +++ b/cyclonedx/model/component.py @@ -1783,7 +1783,7 @@ def __hash__(self) -> int: self.mime_type, self.supplier, self.author, self.publisher, self.description, self.scope, tuple(self.hashes), tuple(self.licenses), self.copyright, self.cpe, - self.purl, + self.purl, self.bom_ref.value, self.swid, self.pedigree, tuple(self.external_references), tuple(self.properties), tuple(self.components), self.evidence, self.release_notes, self.modified, diff --git a/tests/_data/own/json/1.5/duplicate_components.json b/tests/_data/own/json/1.5/duplicate_components.json new file mode 100644 index 00000000..93e5516c --- /dev/null +++ b/tests/_data/own/json/1.5/duplicate_components.json @@ -0,0 +1,48 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.5", + "serialNumber": "urn:uuid:66fa5692-2e9d-45c5-830a-ec8ccaf7dcc9", + "version": 1, + "metadata": { + "component": { + "type": "application", + "name": "test" + } + }, + "components": [ + { + "type": "operating-system", + "bom-ref": "test12", + "name": "alpine" + }, + { + "type": "container", + "bom-ref": "test11", + "name": "alpine" + }, + { + "type": "operating-system", + "bom-ref": "test22", + "name": "alpine" + }, + { + "type": "container", + "bom-ref": "test21", + "name": "alpine" + } + ], + "dependencies": [ + { + "ref": "test11", + "dependsOn": [ + "test12" + ] + }, + { + "ref": "test21", + "dependsOn": [ + "test22" + ] + } + ] +} diff --git a/tests/test_model_component.py b/tests/test_model_component.py index c25fdc91..6efa3bb5 100644 --- a/tests/test_model_component.py +++ b/tests/test_model_component.py @@ -219,6 +219,16 @@ def test_component_equal_3(self) -> None: self.assertNotEqual(c, c2) + def test_component_equal_4(self) -> None: + c = Component( + name='test-component', version='1.2.3', bom_ref='ref1' + ) + c2 = Component( + name='test-component', version='1.2.3', bom_ref='ref2' + ) + + self.assertNotEqual(c, c2) + def test_same_1(self) -> None: c1 = get_component_setuptools_simple() c2 = get_component_setuptools_simple() diff --git a/tests/test_real_world_examples.py b/tests/test_real_world_examples.py index 757d33eb..da7a2feb 100644 --- a/tests/test_real_world_examples.py +++ b/tests/test_real_world_examples.py @@ -17,6 +17,7 @@ import unittest from datetime import datetime +from json import loads as json_loads from os.path import join from typing import Any from unittest.mock import patch @@ -36,3 +37,11 @@ def test_webgoat_6_1(self, *_: Any, **__: Any) -> None: def test_regression_issue_630(self, *_: Any, **__: Any) -> None: with open(join(OWN_DATA_DIRECTORY, 'xml', '1.6', 'regression_issue630.xml')) as input_xml: Bom.from_xml(input_xml) + + def test_merged_bom_duplicate_component(self, *_: Any, **__: Any) -> None: + with open(join(OWN_DATA_DIRECTORY, 'json', '1.5', 'duplicate_components.json')) as input_json: + json = json_loads(input_json.read()) + + bom = Bom.from_json(json) + self.assertEqual(4, len(bom.components)) # tests https://github.com/CycloneDX/cyclonedx-python-lib/issues/540 + bom.validate() # tests https://github.com/CycloneDX/cyclonedx-python-lib/issues/677