Skip to content

Commit c8175bb

Browse files
authored
fix: conditional warning if no root dependencies were found (#398)
Signed-off-by: Jan Kowalleck <[email protected]>
1 parent 1d262ba commit c8175bb

9 files changed

+239
-86
lines changed

.isort.cfg

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ skip_glob =
99
.git/*,.tox/*,.venv/*,venv/*,.venv*/*,venv*/*,
1010
_OLD/*,_TEST/*,
1111
docs/*
12+
examples/*
1213
combine_as_imports = true
1314
default_section = THIRDPARTY
1415
ensure_newline_before_comments = true

cyclonedx/model/bom.py

+6-5
Original file line numberDiff line numberDiff line change
@@ -562,14 +562,15 @@ def validate(self) -> bool:
562562
f'One or more Components have Dependency references to Components/Services that are not known in this '
563563
f'BOM. They are: {dependency_diff}')
564564

565-
# 2. Dependencies should exist for the Component this BOM is describing, if one is set
566-
if self.metadata.component and filter(
567-
lambda _d: _d.ref == self.metadata.component.bom_ref, self.dependencies # type: ignore[arg-type]
568-
):
565+
# 2. if root component is set: dependencies should exist for the Component this BOM is describing
566+
if self.metadata.component and not any(map(
567+
lambda d: d.ref == self.metadata.component.bom_ref and len(d.dependencies) > 0, # type: ignore[union-attr]
568+
self.dependencies
569+
)):
569570
warnings.warn(
570571
f'The Component this BOM is describing {self.metadata.component.purl} has no defined dependencies '
571572
f'which means the Dependency Graph is incomplete - you should add direct dependencies to this '
572-
f'Component to complete the Dependency Graph data.',
573+
f'"root" Component to complete the Dependency Graph data.',
573574
UserWarning
574575
)
575576

cyclonedx/model/dependency.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ class Dependency:
5858

5959
def __init__(self, ref: BomRef, dependencies: Optional[Iterable["Dependency"]] = None) -> None:
6060
self.ref = ref
61-
self.dependencies = SortedSet(dependencies) or SortedSet()
61+
self.dependencies = SortedSet(dependencies or [])
6262

6363
@property # type: ignore[misc]
6464
@serializable.type_mapping(BomRefHelper)

tests/data.py

+30-1
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444
Tool,
4545
XsUri,
4646
)
47-
from cyclonedx.model.bom import Bom
47+
from cyclonedx.model.bom import Bom, BomMetaData
4848
from cyclonedx.model.component import (
4949
Commit,
5050
Component,
@@ -131,6 +131,35 @@ def get_bom_with_dependencies_valid() -> Bom:
131131
)
132132

133133

134+
def get_bom_with_dependencies_hanging() -> Bom:
135+
"""
136+
A bom with a RootComponent and components,
137+
but no dependencies are connected to RootComponent.
138+
"""
139+
c1 = get_component_setuptools_simple('setuptools')
140+
c2 = get_component_toml_with_hashes_with_references('toml')
141+
bom = Bom(
142+
serial_number=UUID(hex='12345678395b41f5a30f1234567890ab'),
143+
version=23,
144+
metadata=BomMetaData(
145+
component=Component(name='rootComponent', type=ComponentType.APPLICATION, bom_ref='root-component'),
146+
),
147+
components=[c1, c2],
148+
dependencies=[
149+
Dependency(c1.bom_ref, [
150+
Dependency(c2.bom_ref)
151+
]),
152+
Dependency(c2.bom_ref)
153+
]
154+
)
155+
bom.metadata.tools.clear()
156+
bom.metadata.timestamp = datetime(
157+
year=2023, month=6, day=1,
158+
hour=3, minute=3, second=7, microsecond=0,
159+
tzinfo=timezone.utc)
160+
return bom
161+
162+
134163
def get_bom_with_dependencies_invalid() -> Bom:
135164
c1 = get_component_setuptools_simple()
136165
return Bom(components=[c1], dependencies=[Dependency(ref=c1.bom_ref)])
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
{
2+
"$schema": "http://cyclonedx.org/schema/bom-1.4.schema.json",
3+
"bomFormat": "CycloneDX",
4+
"specVersion": "1.4",
5+
"serialNumber": "urn:uuid:12345678-395b-41f5-a30f-1234567890ab",
6+
"version": 23,
7+
"metadata": {
8+
"component": {
9+
"bom-ref": "root-component",
10+
"name": "rootComponent",
11+
"type": "application"
12+
},
13+
"timestamp": "2023-06-01T03:03:07+00:00"
14+
},
15+
"components": [
16+
{
17+
"author": "Test Author",
18+
"bom-ref": "setuptools",
19+
"licenses": [
20+
{
21+
"expression": "MIT License"
22+
}
23+
],
24+
"name": "setuptools",
25+
"purl": "pkg:pypi/[email protected]?extension=tar.gz",
26+
"type": "library",
27+
"version": "50.3.2"
28+
},
29+
{
30+
"bom-ref": "toml",
31+
"externalReferences": [
32+
{
33+
"comment": "No comment",
34+
"hashes": [
35+
{
36+
"alg": "SHA-256",
37+
"content": "806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"
38+
}
39+
],
40+
"type": "distribution",
41+
"url": "https://cyclonedx.org"
42+
}
43+
],
44+
"hashes": [
45+
{
46+
"alg": "SHA-256",
47+
"content": "806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"
48+
}
49+
],
50+
"name": "toml",
51+
"purl": "pkg:pypi/[email protected]?extension=tar.gz",
52+
"type": "library",
53+
"version": "0.10.2"
54+
}
55+
],
56+
"dependencies": [
57+
{
58+
"ref": "root-component"
59+
},
60+
{
61+
"ref": "setuptools",
62+
"dependsOn": [
63+
"toml"
64+
]
65+
},
66+
{
67+
"ref": "toml"
68+
}
69+
]
70+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<bom xmlns="http://cyclonedx.org/schema/bom/1.4"
3+
serialNumber="urn:uuid:12345678-395b-41f5-a30f-1234567890ab"
4+
version="23">
5+
<metadata>
6+
<timestamp>2023-06-01T03:03:07+00:00</timestamp>
7+
<component type="application" bom-ref="root-component">
8+
<name>rootComponent</name>
9+
</component>
10+
</metadata>
11+
<components>
12+
<component type="library" bom-ref="setuptools">
13+
<author>Test Author</author>
14+
<name>setuptools</name>
15+
<version>50.3.2</version>
16+
<licenses>
17+
<expression>MIT License</expression>
18+
</licenses>
19+
<purl>pkg:pypi/[email protected]?extension=tar.gz</purl>
20+
</component>
21+
<component type="library" bom-ref="toml">
22+
<name>toml</name>
23+
<version>0.10.2</version>
24+
<hashes>
25+
<hash alg="SHA-256">806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b</hash>
26+
</hashes>
27+
<purl>pkg:pypi/[email protected]?extension=tar.gz</purl>
28+
<externalReferences>
29+
<reference type="distribution">
30+
<url>https://cyclonedx.org</url>
31+
<comment>No comment</comment>
32+
<hashes>
33+
<hash alg="SHA-256">806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b</hash>
34+
</hashes>
35+
</reference>
36+
</externalReferences>
37+
</component>
38+
</components>
39+
<dependencies>
40+
<dependency ref="root-component"/>
41+
<dependency ref="setuptools">
42+
<dependency ref="toml"/>
43+
</dependency>
44+
<dependency ref="toml"/>
45+
</dependencies>
46+
</bom>
47+

tests/test_deserialize_xml.py

+35-42
Original file line numberDiff line numberDiff line change
@@ -623,66 +623,59 @@ def test_bom_v1_4_dependencies_for_bom_component(self, mock_uuid: Mock) -> None:
623623

624624
@patch('cyclonedx.model.bom.uuid4', return_value=MOCK_BOM_UUID_1)
625625
def test_bom_v1_3_dependencies_for_bom_component(self, mock_uuid: Mock) -> None:
626-
with self.assertWarns(UserWarning):
627-
self._validate_xml_bom(
628-
bom=get_bom_with_metadata_component_and_dependencies(), schema_version=SchemaVersion.V1_3,
629-
fixture='bom_dependencies_component.xml'
630-
)
631-
mock_uuid.assert_called()
626+
self._validate_xml_bom(
627+
bom=get_bom_with_metadata_component_and_dependencies(), schema_version=SchemaVersion.V1_3,
628+
fixture='bom_dependencies_component.xml'
629+
)
630+
mock_uuid.assert_called()
632631

633632
@patch('cyclonedx.model.bom.uuid4', return_value=MOCK_BOM_UUID_1)
634633
def test_bom_v1_2_dependencies_for_bom_component(self, mock_uuid: Mock) -> None:
635-
with self.assertWarns(UserWarning):
636-
self._validate_xml_bom(
637-
bom=get_bom_with_metadata_component_and_dependencies(), schema_version=SchemaVersion.V1_2,
638-
fixture='bom_dependencies_component.xml'
639-
)
640-
mock_uuid.assert_called()
634+
self._validate_xml_bom(
635+
bom=get_bom_with_metadata_component_and_dependencies(), schema_version=SchemaVersion.V1_2,
636+
fixture='bom_dependencies_component.xml'
637+
)
638+
mock_uuid.assert_called()
641639

642640
@patch('cyclonedx.model.bom.uuid4', return_value=MOCK_BOM_UUID_1)
643641
def test_bom_v1_4_issue_275_components(self, mock_uuid: Mock) -> None:
644-
with self.assertWarns(UserWarning):
645-
self._validate_xml_bom(
646-
bom=get_bom_for_issue_275_components(), schema_version=SchemaVersion.V1_4,
647-
fixture='bom_issue_275_components.xml'
648-
)
649-
mock_uuid.assert_called()
642+
self._validate_xml_bom(
643+
bom=get_bom_for_issue_275_components(), schema_version=SchemaVersion.V1_4,
644+
fixture='bom_issue_275_components.xml'
645+
)
646+
mock_uuid.assert_called()
650647

651648
@patch('cyclonedx.model.bom.uuid4', return_value=MOCK_BOM_UUID_1)
652649
def test_bom_v1_3_issue_275_components(self, mock_uuid: Mock) -> None:
653-
with self.assertWarns(UserWarning):
654-
self._validate_xml_bom(
655-
bom=get_bom_for_issue_275_components(), schema_version=SchemaVersion.V1_3,
656-
fixture='bom_issue_275_components.xml'
657-
)
658-
mock_uuid.assert_called()
650+
self._validate_xml_bom(
651+
bom=get_bom_for_issue_275_components(), schema_version=SchemaVersion.V1_3,
652+
fixture='bom_issue_275_components.xml'
653+
)
654+
mock_uuid.assert_called()
659655

660656
@patch('cyclonedx.model.bom.uuid4', return_value=MOCK_BOM_UUID_1)
661657
def test_bom_v1_2_issue_275_components(self, mock_uuid: Mock) -> None:
662-
with self.assertWarns(UserWarning):
663-
self._validate_xml_bom(
664-
bom=get_bom_for_issue_275_components(), schema_version=SchemaVersion.V1_2,
665-
fixture='bom_issue_275_components.xml'
666-
)
667-
mock_uuid.assert_called()
658+
self._validate_xml_bom(
659+
bom=get_bom_for_issue_275_components(), schema_version=SchemaVersion.V1_2,
660+
fixture='bom_issue_275_components.xml'
661+
)
662+
mock_uuid.assert_called()
668663

669664
@patch('cyclonedx.model.bom.uuid4', return_value=MOCK_BOM_UUID_1)
670665
def test_bom_v1_1_issue_275_components(self, mock_uuid: Mock) -> None:
671-
with self.assertWarns(UserWarning):
672-
self._validate_xml_bom(
673-
bom=get_bom_for_issue_275_components(), schema_version=SchemaVersion.V1_1,
674-
fixture='bom_issue_275_components.xml'
675-
)
676-
mock_uuid.assert_called()
666+
self._validate_xml_bom(
667+
bom=get_bom_for_issue_275_components(), schema_version=SchemaVersion.V1_1,
668+
fixture='bom_issue_275_components.xml'
669+
)
670+
mock_uuid.assert_called()
677671

678672
@patch('cyclonedx.model.bom.uuid4', return_value=MOCK_BOM_UUID_1)
679673
def test_bom_v1_0_issue_275_components(self, mock_uuid: Mock) -> None:
680-
with self.assertWarns(UserWarning):
681-
self._validate_xml_bom(
682-
bom=get_bom_for_issue_275_components(), schema_version=SchemaVersion.V1_0,
683-
fixture='bom_issue_275_components.xml'
684-
)
685-
mock_uuid.assert_called()
674+
self._validate_xml_bom(
675+
bom=get_bom_for_issue_275_components(), schema_version=SchemaVersion.V1_0,
676+
fixture='bom_issue_275_components.xml'
677+
)
678+
mock_uuid.assert_called()
686679

687680
# Helper methods
688681
def _validate_xml_bom(self, bom: Bom, schema_version: SchemaVersion, fixture: str) -> None:

tests/test_output_json.py

+10
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
get_bom_with_component_setuptools_with_release_notes,
4343
get_bom_with_component_setuptools_with_vulnerability,
4444
get_bom_with_component_toml_1,
45+
get_bom_with_dependencies_hanging,
4546
get_bom_with_dependencies_valid,
4647
get_bom_with_external_references,
4748
get_bom_with_metadata_component_and_dependencies,
@@ -384,6 +385,15 @@ def test_bom_v1_2_issue_275_components(self) -> None:
384385
fixture='bom_issue_275_components.json'
385386
)
386387

388+
def test_bom_v1_4_warn_dependencies(self) -> None:
389+
with self.assertWarns(UserWarning):
390+
# this data set is expected to throw this UserWarning.
391+
# that is the while point of this test!
392+
self._validate_json_bom(
393+
bom=get_bom_with_dependencies_hanging(), schema_version=SchemaVersion.V1_4,
394+
fixture='bom_with_dependencies_hanging.json'
395+
)
396+
387397
# region Helper methods
388398

389399
def _validate_json_bom(self, bom: Bom, schema_version: SchemaVersion, fixture: str) -> None:

0 commit comments

Comments
 (0)