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
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
@@ -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}'
@@ -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:
45 changes: 39 additions & 6 deletions cyclonedx/model/dependency.py
Original file line number Diff line number Diff line change
@@ -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'<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):
22 changes: 22 additions & 0 deletions tests/_data/models.py
Original file line number Diff line number Diff line change
@@ -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(
ref=c3.bom_ref
),
],
)


# ---


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/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"
}
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/setuptools@50.3.2?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/toml@0.10.2?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
@@ -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)