Skip to content

Commit

Permalink
fix: conditional warning if no root dependencies were found (#398)
Browse files Browse the repository at this point in the history

Signed-off-by: Jan Kowalleck <jan.kowalleck@gmail.com>
  • Loading branch information
jkowalleck authored Jun 28, 2023
1 parent 1d262ba commit c8175bb
Show file tree
Hide file tree
Showing 9 changed files with 239 additions and 86 deletions.
1 change: 1 addition & 0 deletions .isort.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ skip_glob =
.git/*,.tox/*,.venv/*,venv/*,.venv*/*,venv*/*,
_OLD/*,_TEST/*,
docs/*
examples/*
combine_as_imports = true
default_section = THIRDPARTY
ensure_newline_before_comments = true
Expand Down
11 changes: 6 additions & 5 deletions cyclonedx/model/bom.py
Original file line number Diff line number Diff line change
Expand Up @@ -562,14 +562,15 @@ def validate(self) -> bool:
f'One or more Components have Dependency references to Components/Services that are not known in this '
f'BOM. They are: {dependency_diff}')

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

Expand Down
2 changes: 1 addition & 1 deletion cyclonedx/model/dependency.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ class Dependency:

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

@property # type: ignore[misc]
@serializable.type_mapping(BomRefHelper)
Expand Down
31 changes: 30 additions & 1 deletion tests/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
Tool,
XsUri,
)
from cyclonedx.model.bom import Bom
from cyclonedx.model.bom import Bom, BomMetaData
from cyclonedx.model.component import (
Commit,
Component,
Expand Down Expand Up @@ -131,6 +131,35 @@ def get_bom_with_dependencies_valid() -> Bom:
)


def get_bom_with_dependencies_hanging() -> Bom:
"""
A bom with a RootComponent and components,
but no dependencies are connected to RootComponent.
"""
c1 = get_component_setuptools_simple('setuptools')
c2 = get_component_toml_with_hashes_with_references('toml')
bom = Bom(
serial_number=UUID(hex='12345678395b41f5a30f1234567890ab'),
version=23,
metadata=BomMetaData(
component=Component(name='rootComponent', type=ComponentType.APPLICATION, bom_ref='root-component'),
),
components=[c1, c2],
dependencies=[
Dependency(c1.bom_ref, [
Dependency(c2.bom_ref)
]),
Dependency(c2.bom_ref)
]
)
bom.metadata.tools.clear()
bom.metadata.timestamp = datetime(
year=2023, month=6, day=1,
hour=3, minute=3, second=7, microsecond=0,
tzinfo=timezone.utc)
return bom


def get_bom_with_dependencies_invalid() -> Bom:
c1 = get_component_setuptools_simple()
return Bom(components=[c1], dependencies=[Dependency(ref=c1.bom_ref)])
Expand Down
70 changes: 70 additions & 0 deletions tests/fixtures/json/1.4/bom_with_dependencies_hanging.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
{
"$schema": "http://cyclonedx.org/schema/bom-1.4.schema.json",
"bomFormat": "CycloneDX",
"specVersion": "1.4",
"serialNumber": "urn:uuid:12345678-395b-41f5-a30f-1234567890ab",
"version": 23,
"metadata": {
"component": {
"bom-ref": "root-component",
"name": "rootComponent",
"type": "application"
},
"timestamp": "2023-06-01T03:03:07+00:00"
},
"components": [
{
"author": "Test Author",
"bom-ref": "setuptools",
"licenses": [
{
"expression": "MIT License"
}
],
"name": "setuptools",
"purl": "pkg:pypi/setuptools@50.3.2?extension=tar.gz",
"type": "library",
"version": "50.3.2"
},
{
"bom-ref": "toml",
"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": "root-component"
},
{
"ref": "setuptools",
"dependsOn": [
"toml"
]
},
{
"ref": "toml"
}
]
}
47 changes: 47 additions & 0 deletions tests/fixtures/xml/1.4/bom_with_dependencies_hanging.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8"?>
<bom xmlns="http://cyclonedx.org/schema/bom/1.4"
serialNumber="urn:uuid:12345678-395b-41f5-a30f-1234567890ab"
version="23">
<metadata>
<timestamp>2023-06-01T03:03:07+00:00</timestamp>
<component type="application" bom-ref="root-component">
<name>rootComponent</name>
</component>
</metadata>
<components>
<component type="library" bom-ref="setuptools">
<author>Test Author</author>
<name>setuptools</name>
<version>50.3.2</version>
<licenses>
<expression>MIT License</expression>
</licenses>
<purl>pkg:pypi/setuptools@50.3.2?extension=tar.gz</purl>
</component>
<component type="library" bom-ref="toml">
<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="root-component"/>
<dependency ref="setuptools">
<dependency ref="toml"/>
</dependency>
<dependency ref="toml"/>
</dependencies>
</bom>

77 changes: 35 additions & 42 deletions tests/test_deserialize_xml.py
Original file line number Diff line number Diff line change
Expand Up @@ -623,66 +623,59 @@ def test_bom_v1_4_dependencies_for_bom_component(self, mock_uuid: Mock) -> None:

@patch('cyclonedx.model.bom.uuid4', return_value=MOCK_BOM_UUID_1)
def test_bom_v1_3_dependencies_for_bom_component(self, mock_uuid: Mock) -> None:
with self.assertWarns(UserWarning):
self._validate_xml_bom(
bom=get_bom_with_metadata_component_and_dependencies(), schema_version=SchemaVersion.V1_3,
fixture='bom_dependencies_component.xml'
)
mock_uuid.assert_called()
self._validate_xml_bom(
bom=get_bom_with_metadata_component_and_dependencies(), schema_version=SchemaVersion.V1_3,
fixture='bom_dependencies_component.xml'
)
mock_uuid.assert_called()

@patch('cyclonedx.model.bom.uuid4', return_value=MOCK_BOM_UUID_1)
def test_bom_v1_2_dependencies_for_bom_component(self, mock_uuid: Mock) -> None:
with self.assertWarns(UserWarning):
self._validate_xml_bom(
bom=get_bom_with_metadata_component_and_dependencies(), schema_version=SchemaVersion.V1_2,
fixture='bom_dependencies_component.xml'
)
mock_uuid.assert_called()
self._validate_xml_bom(
bom=get_bom_with_metadata_component_and_dependencies(), schema_version=SchemaVersion.V1_2,
fixture='bom_dependencies_component.xml'
)
mock_uuid.assert_called()

@patch('cyclonedx.model.bom.uuid4', return_value=MOCK_BOM_UUID_1)
def test_bom_v1_4_issue_275_components(self, mock_uuid: Mock) -> None:
with self.assertWarns(UserWarning):
self._validate_xml_bom(
bom=get_bom_for_issue_275_components(), schema_version=SchemaVersion.V1_4,
fixture='bom_issue_275_components.xml'
)
mock_uuid.assert_called()
self._validate_xml_bom(
bom=get_bom_for_issue_275_components(), schema_version=SchemaVersion.V1_4,
fixture='bom_issue_275_components.xml'
)
mock_uuid.assert_called()

@patch('cyclonedx.model.bom.uuid4', return_value=MOCK_BOM_UUID_1)
def test_bom_v1_3_issue_275_components(self, mock_uuid: Mock) -> None:
with self.assertWarns(UserWarning):
self._validate_xml_bom(
bom=get_bom_for_issue_275_components(), schema_version=SchemaVersion.V1_3,
fixture='bom_issue_275_components.xml'
)
mock_uuid.assert_called()
self._validate_xml_bom(
bom=get_bom_for_issue_275_components(), schema_version=SchemaVersion.V1_3,
fixture='bom_issue_275_components.xml'
)
mock_uuid.assert_called()

@patch('cyclonedx.model.bom.uuid4', return_value=MOCK_BOM_UUID_1)
def test_bom_v1_2_issue_275_components(self, mock_uuid: Mock) -> None:
with self.assertWarns(UserWarning):
self._validate_xml_bom(
bom=get_bom_for_issue_275_components(), schema_version=SchemaVersion.V1_2,
fixture='bom_issue_275_components.xml'
)
mock_uuid.assert_called()
self._validate_xml_bom(
bom=get_bom_for_issue_275_components(), schema_version=SchemaVersion.V1_2,
fixture='bom_issue_275_components.xml'
)
mock_uuid.assert_called()

@patch('cyclonedx.model.bom.uuid4', return_value=MOCK_BOM_UUID_1)
def test_bom_v1_1_issue_275_components(self, mock_uuid: Mock) -> None:
with self.assertWarns(UserWarning):
self._validate_xml_bom(
bom=get_bom_for_issue_275_components(), schema_version=SchemaVersion.V1_1,
fixture='bom_issue_275_components.xml'
)
mock_uuid.assert_called()
self._validate_xml_bom(
bom=get_bom_for_issue_275_components(), schema_version=SchemaVersion.V1_1,
fixture='bom_issue_275_components.xml'
)
mock_uuid.assert_called()

@patch('cyclonedx.model.bom.uuid4', return_value=MOCK_BOM_UUID_1)
def test_bom_v1_0_issue_275_components(self, mock_uuid: Mock) -> None:
with self.assertWarns(UserWarning):
self._validate_xml_bom(
bom=get_bom_for_issue_275_components(), schema_version=SchemaVersion.V1_0,
fixture='bom_issue_275_components.xml'
)
mock_uuid.assert_called()
self._validate_xml_bom(
bom=get_bom_for_issue_275_components(), schema_version=SchemaVersion.V1_0,
fixture='bom_issue_275_components.xml'
)
mock_uuid.assert_called()

# Helper methods
def _validate_xml_bom(self, bom: Bom, schema_version: SchemaVersion, fixture: str) -> None:
Expand Down
10 changes: 10 additions & 0 deletions tests/test_output_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
get_bom_with_component_setuptools_with_release_notes,
get_bom_with_component_setuptools_with_vulnerability,
get_bom_with_component_toml_1,
get_bom_with_dependencies_hanging,
get_bom_with_dependencies_valid,
get_bom_with_external_references,
get_bom_with_metadata_component_and_dependencies,
Expand Down Expand Up @@ -384,6 +385,15 @@ def test_bom_v1_2_issue_275_components(self) -> None:
fixture='bom_issue_275_components.json'
)

def test_bom_v1_4_warn_dependencies(self) -> None:
with self.assertWarns(UserWarning):
# this data set is expected to throw this UserWarning.
# that is the while point of this test!
self._validate_json_bom(
bom=get_bom_with_dependencies_hanging(), schema_version=SchemaVersion.V1_4,
fixture='bom_with_dependencies_hanging.json'
)

# region Helper methods

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

0 comments on commit c8175bb

Please sign in to comment.