Skip to content
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

feat: add cyclonedx.model.dependency.Dependency.provides #735

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
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
29 changes: 22 additions & 7 deletions cyclonedx/model/bom.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead of adding a new parameter here, how about adding a new method instead: register_provision(self, target: Dependable, provides: Optional[Iterable[Dependable]] = None).

what do you think about this?
this would fit the original architectural plans better.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll give this a try

) -> 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}'
Expand All @@ -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:
Expand Down
45 changes: 39 additions & 6 deletions cyclonedx/model/dependency.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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'<Dependency ref={self.ref!r}, targets={len(self.dependencies)}>'
return (
f'<Dependency ref={self.ref!r}'
f', targets={len(self.dependencies)}'
f', provides={len(self.provides)}>'
)


class Dependable(ABC):
Expand Down
22 changes: 22 additions & 0 deletions tests/_data/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1310,6 +1310,28 @@ def get_bom_with_definitions_standards() -> Bom:
)


def get_bom_v1_6_with_provides() -> Bom:
Copy link
Member

@jkowalleck jkowalleck Nov 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please rename to get_bom_with_provides.


there is no intention to have models for certain CDX versions only.
In fact, it is intended to test the serialization with a target that is expected to omit certain parts.

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(
uzairchhapra marked this conversation as resolved.
Show resolved Hide resolved
ref=c3.bom_ref
),
],
)


# ---


Expand Down
113 changes: 113 additions & 0 deletions tests/_data/snapshots/get_bom_v1_6_with_provides-1.6.json.bin
Original file line number Diff line number Diff line change
@@ -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/[email protected]?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/[email protected]?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"
}
77 changes: 77 additions & 0 deletions tests/_data/snapshots/get_bom_v1_6_with_provides-1.6.xml.bin
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<?xml version="1.0" ?>
<bom xmlns="http://cyclonedx.org/schema/bom/1.6" serialNumber="urn:uuid:1441d33a-e0fc-45b5-af3b-61ee52a88bac" version="1">
<metadata>
<timestamp>2023-01-07T13:44:32.312678+00:00</timestamp>
</metadata>
<components>
<component type="cryptographic-asset" bom-ref="crypto-algorithm">
<name>My Algorithm</name>
<version>1.0</version>
<cryptoProperties>
<assetType>algorithm</assetType>
<algorithmProperties>
<primitive>kem</primitive>
<parameterSetIdentifier>a-parameter-set-id</parameterSetIdentifier>
<curve>9n8y2oxty3ao83n8qc2g2x3qcw4jt4wj</curve>
<executionEnvironment>software-plain-ram</executionEnvironment>
<implementationPlatform>generic</implementationPlatform>
<certificationLevel>fips140-1-l1</certificationLevel>
<certificationLevel>fips140-2-l3</certificationLevel>
<certificationLevel>other</certificationLevel>
<mode>ecb</mode>
<padding>pkcs7</padding>
<cryptoFunctions>
<cryptoFunction>sign</cryptoFunction>
<cryptoFunction>unknown</cryptoFunction>
</cryptoFunctions>
<classicalSecurityLevel>2</classicalSecurityLevel>
<nistQuantumSecurityLevel>2</nistQuantumSecurityLevel>
</algorithmProperties>
<oid>an-oid-here</oid>
</cryptoProperties>
<tags>
<tag>algorithm</tag>
</tags>
</component>
<component type="library" bom-ref="some-library">
<author>Test Author</author>
<name>setuptools</name>
<version>50.3.2</version>
<licenses>
<license>
<id>MIT</id>
</license>
</licenses>
<purl>pkg:pypi/[email protected]?extension=tar.gz</purl>
</component>
<component type="library" bom-ref="crypto-library">
<name>toml</name>
<version>0.10.2</version>
<hashes>
<hash alg="SHA-256">806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b</hash>
</hashes>
<purl>pkg:pypi/[email protected]?extension=tar.gz</purl>
<externalReferences>
<reference type="distribution">
<url>https://cyclonedx.org</url>
<comment>No comment</comment>
<hashes>
<hash alg="SHA-256">806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b</hash>
</hashes>
</reference>
</externalReferences>
</component>
</components>
<dependencies>
<dependency ref="crypto-algorithm"/>
<dependency ref="crypto-library">
<dependency ref="some-library"/>
<provides ref="crypto-algorithm"/>
</dependency>
<dependency ref="some-library"/>
</dependencies>
<properties>
<property name="key1">val1</property>
<property name="key2">val2</property>
</properties>
</bom>
23 changes: 23 additions & 0 deletions tests/test_model_dependency.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)