Skip to content

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

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

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

Copy link
Author

Choose a reason for hiding this comment

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

@jkowalleck
How should this function work register_provision for the example mentioned pin models.py?

A -- requires -> C
B -- provides -> C

Considering register_dependency and register_provision will be called for all A, B and C, should I look for existing dependencies added in register_provision to avoid creating a new Dependency?

Copy link
Member

Choose a reason for hiding this comment

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

this is still open.
could you consider adding a new method?

) -> 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 @@ -686,12 +699,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
37 changes: 33 additions & 4 deletions cyclonedx/model/dependency.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
import py_serializable as serializable
from sortedcontainers import SortedSet

from cyclonedx.schema.schema import SchemaVersion1Dot6

from .._internal.compare import ComparableTuple as _ComparableTuple
from ..exception.serialization import SerializationOfUnexpectedValueException
from .bom_ref import BomRef
Expand Down Expand Up @@ -52,12 +54,20 @@ class Dependency:
Models a Dependency within a BOM.

.. note::
See https://cyclonedx.org/docs/1.6/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(BomRef)
Expand All @@ -80,14 +90,29 @@ 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 __comparable_tuple(self) -> _ComparableTuple:
return _ComparableTuple((
self.ref, _ComparableTuple(self.dependencies)
self.ref, _ComparableTuple(self.dependencies), _ComparableTuple(self.provides)
))

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 self.__comparable_tuple() == other.__comparable_tuple()
Expand All @@ -102,7 +127,11 @@ def __hash__(self) -> int:
return hash(self.__comparable_tuple())

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 @@ -1406,6 +1406,28 @@ def get_bom_with_definitions_and_detailed_standards() -> Bom:
]))


def get_bom_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
),
],
)


def get_bom_for_issue540_duplicate_components() -> Bom:
# tests https://github.com/CycloneDX/cyclonedx-python-lib/issues/540
bom = _make_bom()
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/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>
Loading
Loading