diff --git a/cyclonedx/model/bom.py b/cyclonedx/model/bom.py index 7b657604..715a95c0 100644 --- a/cyclonedx/model/bom.py +++ b/cyclonedx/model/bom.py @@ -644,23 +644,36 @@ def has_vulnerabilities(self) -> bool: """ return bool(self.vulnerabilities) - def register_dependency(self, target: Dependable, depends_on: Optional[Iterable[Dependable]] = None) -> None: + def register_dependency( + self, + target: Dependable, + depends_on: Optional[Iterable[Dependable]] = None, + provides: Optional[Iterable[Dependable]] = None, + ) -> None: _d = next(filter(lambda _d: _d.ref == target.bom_ref, self.dependencies), None) if _d: # Dependency Target already registered - but it might have new dependencies to add if depends_on: _d.dependencies.update(map(lambda _d: Dependency(ref=_d.bom_ref), depends_on)) + if provides: + _d.provides.update(map(lambda _p: Dependency(ref=_p.bom_ref), provides)) else: # First time we are seeing this target as a Dependency - self._dependencies.add(Dependency( - ref=target.bom_ref, - dependencies=map(lambda _dep: Dependency(ref=_dep.bom_ref), depends_on) if depends_on else [] - )) + self._dependencies.add( + Dependency( + ref=target.bom_ref, + dependencies=map(lambda _dep: Dependency(ref=_dep.bom_ref), depends_on) if depends_on else [], + provides=map(lambda _prov: Dependency(ref=_prov.bom_ref), provides) if provides else [], + ) + ) if depends_on: # Ensure dependents are registered with no further dependents in the DependencyGraph for _d2 in depends_on: self.register_dependency(target=_d2, depends_on=None) + if provides: + for _p2 in provides: + self.register_dependency(target=_p2, depends_on=None, provides=None) def urn(self) -> str: return f'{_BOM_LINK_PREFIX}{self.serial_number}/{self.version}' @@ -681,12 +694,14 @@ def validate(self) -> bool: for _s in self.services: self.register_dependency(target=_s) - # 1. Make sure dependencies are all in this Bom. + # 1. Make sure dependencies and provides are all in this Bom. component_bom_refs = set(map(lambda c: c.bom_ref, self._get_all_components())) | set( map(lambda s: s.bom_ref, self.services)) + dependency_bom_refs = set(chain( (d.ref for d in self.dependencies), - chain.from_iterable(d.dependencies_as_bom_refs() for d in self.dependencies) + chain.from_iterable(d.dependencies_as_bom_refs() for d in self.dependencies), + chain.from_iterable(d.provides_as_bom_refs() for d in self.dependencies) # Include provides refs here )) dependency_diff = dependency_bom_refs - component_bom_refs if len(dependency_diff) > 0: diff --git a/cyclonedx/model/dependency.py b/cyclonedx/model/dependency.py index 4cdfe17e..27a9aab3 100644 --- a/cyclonedx/model/dependency.py +++ b/cyclonedx/model/dependency.py @@ -22,6 +22,8 @@ import serializable from sortedcontainers import SortedSet +from cyclonedx.schema.schema import SchemaVersion1Dot6 + from .._internal.compare import ComparableTuple as _ComparableTuple from ..exception.serialization import SerializationOfUnexpectedValueException from ..serialization import BomRefHelper @@ -53,12 +55,20 @@ class Dependency: Models a Dependency within a BOM. .. note:: - See https://cyclonedx.org/docs/1.4/xml/#type_dependencyType + See: + 1. https://cyclonedx.org/docs/1.6/xml/#type_dependencyType + 2. https://cyclonedx.org/docs/1.6/json/#dependencies """ - def __init__(self, ref: BomRef, dependencies: Optional[Iterable['Dependency']] = None) -> None: + def __init__( + self, + ref: BomRef, + dependencies: Optional[Iterable['Dependency']] = None, + provides: Optional[Iterable['Dependency']] = None + ) -> None: self.ref = ref self.dependencies = dependencies or [] # type:ignore[assignment] + self.provides = provides or [] # type:ignore[assignment] @property @serializable.type_mapping(BomRefHelper) @@ -81,9 +91,24 @@ def dependencies(self) -> 'SortedSet[Dependency]': def dependencies(self, dependencies: Iterable['Dependency']) -> None: self._dependencies = SortedSet(dependencies) + @property + @serializable.view(SchemaVersion1Dot6) + @serializable.json_name('provides') + @serializable.type_mapping(_DependencyRepositorySerializationHelper) + @serializable.xml_array(serializable.XmlArraySerializationType.FLAT, 'provides') + def provides(self) -> 'SortedSet[Dependency]': + return self._provides + + @provides.setter + def provides(self, provides: Iterable['Dependency']) -> None: + self._provides = SortedSet(provides) + def dependencies_as_bom_refs(self) -> Set[BomRef]: return set(map(lambda d: d.ref, self.dependencies)) + def provides_as_bom_refs(self) -> Set[BomRef]: + return set(map(lambda d: d.ref, self.provides)) + def __eq__(self, other: object) -> bool: if isinstance(other, Dependency): return hash(other) == hash(self) @@ -92,17 +117,25 @@ def __eq__(self, other: object) -> bool: def __lt__(self, other: Any) -> bool: if isinstance(other, Dependency): return _ComparableTuple(( - self.ref, _ComparableTuple(self.dependencies) + self.ref, + _ComparableTuple(self.dependencies), + _ComparableTuple(self.provides) )) < _ComparableTuple(( - other.ref, _ComparableTuple(other.dependencies) + other.ref, + _ComparableTuple(other.dependencies), + _ComparableTuple(other.provides) )) return NotImplemented def __hash__(self) -> int: - return hash((self.ref, tuple(self.dependencies))) + return hash((self.ref, tuple(self.dependencies), tuple(self.provides))) def __repr__(self) -> str: - return f'' + return ( + f'' + ) class Dependable(ABC): diff --git a/tests/_data/models.py b/tests/_data/models.py index ffbf7d4a..cfdb9e21 100644 --- a/tests/_data/models.py +++ b/tests/_data/models.py @@ -1310,6 +1310,28 @@ def get_bom_with_definitions_standards() -> Bom: ) +def get_bom_v1_6_with_provides() -> Bom: + c1 = get_component_toml_with_hashes_with_references('crypto-library') + c2 = get_component_setuptools_simple('some-library') + c3 = get_component_crypto_asset_algorithm('crypto-algorithm') + return _make_bom( + components=[c1, c2, c3], + dependencies=[ + Dependency( + ref=c1.bom_ref, + dependencies=[Dependency(ref=c2.bom_ref)], + provides=[Dependency(ref=c3.bom_ref)] + ), + Dependency( + ref=c2.bom_ref + ), + Dependency( + ref=c3.bom_ref + ), + ], + ) + + # --- diff --git a/tests/_data/snapshots/get_bom_v1_6_with_provides-1.6.json.bin b/tests/_data/snapshots/get_bom_v1_6_with_provides-1.6.json.bin new file mode 100644 index 00000000..5aa6bc69 --- /dev/null +++ b/tests/_data/snapshots/get_bom_v1_6_with_provides-1.6.json.bin @@ -0,0 +1,113 @@ +{ + "components": [ + { + "bom-ref": "crypto-algorithm", + "cryptoProperties": { + "algorithmProperties": { + "certificationLevel": [ + "fips140-1-l1", + "fips140-2-l3", + "other" + ], + "classicalSecurityLevel": 2, + "cryptoFunctions": [ + "sign", + "unknown" + ], + "curve": "9n8y2oxty3ao83n8qc2g2x3qcw4jt4wj", + "executionEnvironment": "software-plain-ram", + "implementationPlatform": "generic", + "mode": "ecb", + "nistQuantumSecurityLevel": 2, + "padding": "pkcs7", + "parameterSetIdentifier": "a-parameter-set-id", + "primitive": "kem" + }, + "assetType": "algorithm", + "oid": "an-oid-here" + }, + "name": "My Algorithm", + "tags": [ + "algorithm" + ], + "type": "cryptographic-asset", + "version": "1.0" + }, + { + "author": "Test Author", + "bom-ref": "some-library", + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "name": "setuptools", + "purl": "pkg:pypi/setuptools@50.3.2?extension=tar.gz", + "type": "library", + "version": "50.3.2" + }, + { + "bom-ref": "crypto-library", + "externalReferences": [ + { + "comment": "No comment", + "hashes": [ + { + "alg": "SHA-256", + "content": "806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b" + } + ], + "type": "distribution", + "url": "https://cyclonedx.org" + } + ], + "hashes": [ + { + "alg": "SHA-256", + "content": "806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b" + } + ], + "name": "toml", + "purl": "pkg:pypi/toml@0.10.2?extension=tar.gz", + "type": "library", + "version": "0.10.2" + } + ], + "dependencies": [ + { + "ref": "crypto-algorithm" + }, + { + "dependsOn": [ + "some-library" + ], + "provides": [ + "crypto-algorithm" + ], + "ref": "crypto-library" + }, + { + "ref": "some-library" + } + ], + "metadata": { + "timestamp": "2023-01-07T13:44:32.312678+00:00" + }, + "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_v1_6_with_provides-1.6.xml.bin b/tests/_data/snapshots/get_bom_v1_6_with_provides-1.6.xml.bin new file mode 100644 index 00000000..5722dbf4 --- /dev/null +++ b/tests/_data/snapshots/get_bom_v1_6_with_provides-1.6.xml.bin @@ -0,0 +1,77 @@ + + + + 2023-01-07T13:44:32.312678+00:00 + + + + My Algorithm + 1.0 + + algorithm + + kem + a-parameter-set-id + 9n8y2oxty3ao83n8qc2g2x3qcw4jt4wj + software-plain-ram + generic + fips140-1-l1 + fips140-2-l3 + other + ecb + pkcs7 + + sign + unknown + + 2 + 2 + + an-oid-here + + + algorithm + + + + Test Author + setuptools + 50.3.2 + + + MIT + + + pkg:pypi/setuptools@50.3.2?extension=tar.gz + + + toml + 0.10.2 + + 806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b + + pkg:pypi/toml@0.10.2?extension=tar.gz + + + https://cyclonedx.org + No comment + + 806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b + + + + + + + + + + + + + + + val1 + val2 + + diff --git a/tests/test_model_dependency.py b/tests/test_model_dependency.py index 77f68b79..33e8d518 100644 --- a/tests/test_model_dependency.py +++ b/tests/test_model_dependency.py @@ -41,3 +41,26 @@ def test_sort(self) -> None: sorted_deps = sorted(deps) expected_deps = reorder(deps, expected_order) self.assertEqual(sorted_deps, expected_deps) + + def test_dependency_with_provides(self) -> None: + # Create test data + ref1 = BomRef(value='be2c6502-7e9a-47db-9a66-e34f729810a3') + ref2 = BomRef(value='0b049d09-64c0-4490-a0f5-c84d9aacf857') + provides_ref1 = BomRef(value='cd3e9c95-9d41-49e7-9924-8cf0465ae789') + provides_ref2 = BomRef(value='17e3b199-dc0b-42ef-bfdd-1fa81a1e3eda') + + # Create dependencies with provides + dep1 = Dependency(ref=ref1, provides=[Dependency(ref=provides_ref1)]) + dep2 = Dependency(ref=ref2, provides=[Dependency(ref=provides_ref2)]) + + # Verify provides field + self.assertEqual(len(dep1.provides), 1) + self.assertEqual(len(dep2.provides), 1) + + # Check provides_as_bom_refs + self.assertEqual(dep1.provides_as_bom_refs(), {provides_ref1}) + self.assertEqual(dep2.provides_as_bom_refs(), {provides_ref2}) + + # Verify comparison and hashing + self.assertNotEqual(hash(dep1), hash(dep2)) + self.assertNotEqual(dep1, dep2)