Skip to content
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
20 changes: 16 additions & 4 deletions cyclonedx/model/bom.py
Original file line number Diff line number Diff line change
Expand Up @@ -778,23 +778,34 @@ 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 []
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:
"""
Expand Down Expand Up @@ -824,12 +835,13 @@ 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)
))
dependency_diff = dependency_bom_refs - component_bom_refs
if len(dependency_diff) > 0:
Expand Down
32 changes: 29 additions & 3 deletions cyclonedx/model/dependency.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
import py_serializable as serializable
from sortedcontainers import SortedSet

from cyclonedx.schema.schema import SchemaVersion1Dot6, SchemaVersion1Dot7

from .._internal.compare import ComparableTuple as _ComparableTuple
from ..exception.serialization import SerializationOfUnexpectedValueException
from .bom_ref import BomRef
Expand All @@ -34,6 +36,8 @@ class _DependencyRepositorySerializationHelper(serializable.helpers.BaseHelper):
@classmethod
def serialize(cls, o: Any) -> list[str]:
if isinstance(o, (SortedSet, set)):
if not o:
return []
return [str(i.ref) for i in o]
raise SerializationOfUnexpectedValueException(
f'Attempt to serialize a non-DependencyRepository: {o!r}')
Expand All @@ -56,9 +60,15 @@ class Dependency:
See https://cyclonedx.org/docs/1.7/xml/#type_dependencyType
"""

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 []
self.provides = provides or []

@property
@serializable.type_mapping(BomRef)
Expand All @@ -84,9 +94,25 @@ def dependencies(self, dependencies: Iterable['Dependency']) -> None:
def dependencies_as_bom_refs(self) -> set[BomRef]:
return set(map(lambda d: d.ref, self.dependencies))

@property
@serializable.view(SchemaVersion1Dot6)
@serializable.view(SchemaVersion1Dot7)
@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 provides_as_bom_refs(self) -> set[BomRef]:
return set(map(lambda d: d.ref, self.provides or []))

def __comparable_tuple(self) -> _ComparableTuple:
return _ComparableTuple((
self.ref, _ComparableTuple(self.dependencies)
self.ref, _ComparableTuple(self.dependencies), _ComparableTuple(self.provides or [])
))

def __eq__(self, other: object) -> bool:
Expand All @@ -103,7 +129,7 @@ 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}, targets={len(self.dependencies)}, provides={len(self.provides or [])}>'


class Dependable(ABC):
Expand Down
24 changes: 24 additions & 0 deletions tests/_data/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1573,6 +1573,29 @@ def get_bom_with_definitions_and_detailed_standards() -> Bom:
]))


def get_bom_with_provides() -> Bom:
bom = _make_bom()
bom.metadata.component = root_component = Component(name='app A', bom_ref='A', type=ComponentType.APPLICATION)
bom.components.add(
c1 := Component(name='device B', bom_ref='B', type=ComponentType.DEVICE))
bom.components.add(
c2 := Component(name='device C', bom_ref='C', type=ComponentType.DEVICE))
bom.dependencies = [
Dependency(
ref=c2.bom_ref
),
Dependency(
ref=c1.bom_ref,
provides=[Dependency(ref=c2.bom_ref)]
),
Dependency(
ref=root_component.bom_ref,
dependencies=[Dependency(ref=c2.bom_ref)]
),
]
return bom


def get_bom_for_issue540_duplicate_components() -> Bom:
# tests https://github.com/CycloneDX/cyclonedx-python-lib/issues/540
bom = _make_bom()
Expand Down Expand Up @@ -1716,6 +1739,7 @@ def get_bom_for_issue941_nested_dependencies_irreversible_migrate() -> Bom:
get_bom_with_services_complex,
get_bom_with_services_simple,
get_bom_with_licenses,
get_bom_with_provides,
get_bom_with_multiple_licenses,
get_bom_for_issue_497_urls,
get_bom_with_external_component_1_7,
Expand Down
15 changes: 15 additions & 0 deletions tests/_data/snapshots/get_bom_with_provides-1.0.xml.bin
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?xml version="1.0" ?>
<bom xmlns="http://cyclonedx.org/schema/bom/1.0" version="1">
<components>
<component type="device">
<name>device B</name>
<version/>
<modified>false</modified>
</component>
<component type="device">
<name>device C</name>
<version/>
<modified>false</modified>
</component>
</components>
</bom>
13 changes: 13 additions & 0 deletions tests/_data/snapshots/get_bom_with_provides-1.1.xml.bin
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?xml version="1.0" ?>
<bom xmlns="http://cyclonedx.org/schema/bom/1.1" serialNumber="urn:uuid:1441d33a-e0fc-45b5-af3b-61ee52a88bac" version="1">
<components>
<component type="device" bom-ref="B">
<name>device B</name>
<version/>
</component>
<component type="device" bom-ref="C">
<name>device C</name>
<version/>
</component>
</components>
</bom>
44 changes: 44 additions & 0 deletions tests/_data/snapshots/get_bom_with_provides-1.2.json.bin
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{
"components": [
{
"bom-ref": "B",
"name": "device B",
"type": "device",
"version": ""
},
{
"bom-ref": "C",
"name": "device C",
"type": "device",
"version": ""
}
],
"dependencies": [
{
"dependsOn": [
"C"
],
"ref": "A"
},
{
"ref": "B"
},
{
"ref": "C"
}
],
"metadata": {
"component": {
"bom-ref": "A",
"name": "app A",
"type": "application",
"version": ""
},
"timestamp": "2023-01-07T13:44:32.312678+00:00"
},
"serialNumber": "urn:uuid:1441d33a-e0fc-45b5-af3b-61ee52a88bac",
"version": 1,
"$schema": "http://cyclonedx.org/schema/bom-1.2b.schema.json",
"bomFormat": "CycloneDX",
"specVersion": "1.2"
}
27 changes: 27 additions & 0 deletions tests/_data/snapshots/get_bom_with_provides-1.2.xml.bin
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?xml version="1.0" ?>
<bom xmlns="http://cyclonedx.org/schema/bom/1.2" serialNumber="urn:uuid:1441d33a-e0fc-45b5-af3b-61ee52a88bac" version="1">
<metadata>
<timestamp>2023-01-07T13:44:32.312678+00:00</timestamp>
<component type="application" bom-ref="A">
<name>app A</name>
<version/>
</component>
</metadata>
<components>
<component type="device" bom-ref="B">
<name>device B</name>
<version/>
</component>
<component type="device" bom-ref="C">
<name>device C</name>
<version/>
</component>
</components>
<dependencies>
<dependency ref="A">
<dependency ref="C"/>
</dependency>
<dependency ref="B"/>
<dependency ref="C"/>
</dependencies>
</bom>
44 changes: 44 additions & 0 deletions tests/_data/snapshots/get_bom_with_provides-1.3.json.bin
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{
"components": [
{
"bom-ref": "B",
"name": "device B",
"type": "device",
"version": ""
},
{
"bom-ref": "C",
"name": "device C",
"type": "device",
"version": ""
}
],
"dependencies": [
{
"dependsOn": [
"C"
],
"ref": "A"
},
{
"ref": "B"
},
{
"ref": "C"
}
],
"metadata": {
"component": {
"bom-ref": "A",
"name": "app A",
"type": "application",
"version": ""
},
"timestamp": "2023-01-07T13:44:32.312678+00:00"
},
"serialNumber": "urn:uuid:1441d33a-e0fc-45b5-af3b-61ee52a88bac",
"version": 1,
"$schema": "http://cyclonedx.org/schema/bom-1.3a.schema.json",
"bomFormat": "CycloneDX",
"specVersion": "1.3"
}
27 changes: 27 additions & 0 deletions tests/_data/snapshots/get_bom_with_provides-1.3.xml.bin
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?xml version="1.0" ?>
<bom xmlns="http://cyclonedx.org/schema/bom/1.3" serialNumber="urn:uuid:1441d33a-e0fc-45b5-af3b-61ee52a88bac" version="1">
<metadata>
<timestamp>2023-01-07T13:44:32.312678+00:00</timestamp>
<component type="application" bom-ref="A">
<name>app A</name>
<version/>
</component>
</metadata>
<components>
<component type="device" bom-ref="B">
<name>device B</name>
<version/>
</component>
<component type="device" bom-ref="C">
<name>device C</name>
<version/>
</component>
</components>
<dependencies>
<dependency ref="A">
<dependency ref="C"/>
</dependency>
<dependency ref="B"/>
<dependency ref="C"/>
</dependencies>
</bom>
41 changes: 41 additions & 0 deletions tests/_data/snapshots/get_bom_with_provides-1.4.json.bin
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"components": [
{
"bom-ref": "B",
"name": "device B",
"type": "device"
},
{
"bom-ref": "C",
"name": "device C",
"type": "device"
}
],
"dependencies": [
{
"dependsOn": [
"C"
],
"ref": "A"
},
{
"ref": "B"
},
{
"ref": "C"
}
],
"metadata": {
"component": {
"bom-ref": "A",
"name": "app A",
"type": "application"
},
"timestamp": "2023-01-07T13:44:32.312678+00:00"
},
"serialNumber": "urn:uuid:1441d33a-e0fc-45b5-af3b-61ee52a88bac",
"version": 1,
"$schema": "http://cyclonedx.org/schema/bom-1.4.schema.json",
"bomFormat": "CycloneDX",
"specVersion": "1.4"
}
Loading