Skip to content

Commit 68a0cdd

Browse files
authored
Merge pull request #276 from CycloneDX/fix/bom-validation-nested-components-isue-275
fix: BOM validation fails when Components or Services are nested #275 fix: updated dependencies #271, #270, #269 and #256
2 parents 01cb53b + 6caee65 commit 68a0cdd

32 files changed

+2187
-636
lines changed

.isort.cfg

+4-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
## read the docs: https://pycqa.github.io/isort/docs/configuration/options.html
33
## keep in sync with flake8 config - in `tox.ini` file
44
known_first_party = cyclonedx
5-
skip_gitignore = true
5+
skip_gitignore = false
66
skip_glob =
77
build/*,dist/*,__pycache__,.eggs,*.egg-info*,
88
*_cache,*.cache,
@@ -15,3 +15,6 @@ ensure_newline_before_comments = true
1515
include_trailing_comma = true
1616
line_length = 120
1717
multi_line_output = 3
18+
src_paths =
19+
cyclonedx
20+
tests

.pre-commit-config.yaml

+7-7
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@ repos:
77
entry: poetry run tox -e mypy
88
pass_filenames: false
99
language: system
10-
# - repo: local
11-
# hooks:
12-
# - id: system
13-
# name: isort
14-
# entry: poetry run isort
15-
# pass_filenames: false
16-
# language: system
10+
- repo: local
11+
hooks:
12+
- id: system
13+
name: isort
14+
entry: poetry run isort -c .
15+
pass_filenames: false
16+
language: system
1717
- repo: local
1818
hooks:
1919
- id: system

cyclonedx/model/bom.py

+16-6
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
# Copyright (c) OWASP Foundation. All Rights Reserved.
1919
import warnings
2020
from datetime import datetime, timezone
21-
from typing import Iterable, Optional
21+
from typing import Iterable, Optional, Set
2222
from uuid import UUID, uuid4
2323

2424
from sortedcontainers import SortedSet
@@ -356,6 +356,16 @@ def external_references(self) -> "SortedSet[ExternalReference]":
356356
def external_references(self, external_references: Iterable[ExternalReference]) -> None:
357357
self._external_references = SortedSet(external_references)
358358

359+
def _get_all_components(self) -> Set[Component]:
360+
components: Set[Component] = set()
361+
if self.metadata.component:
362+
components.update(self.metadata.component.get_all_nested_components(include_self=True))
363+
364+
for c in self.components:
365+
components.update(c.get_all_nested_components(include_self=True))
366+
367+
return components
368+
359369
def has_vulnerabilities(self) -> bool:
360370
"""
361371
Check whether this Bom has any declared vulnerabilities.
@@ -376,8 +386,8 @@ def validate(self) -> bool:
376386
"""
377387

378388
# 1. Make sure dependencies are all in this Bom.
379-
all_bom_refs = set([self.metadata.component.bom_ref] if self.metadata.component else []) | set(
380-
map(lambda c: c.bom_ref, self.components)) | set(map(lambda s: s.bom_ref, self.services))
389+
all_bom_refs = set(map(lambda c: c.bom_ref, self._get_all_components())) | set(
390+
map(lambda s: s.bom_ref, self.services))
381391

382392
all_dependency_bom_refs = set().union(*(c.dependencies for c in self.components))
383393
dependency_diff = all_dependency_bom_refs - all_bom_refs
@@ -389,9 +399,9 @@ def validate(self) -> bool:
389399
# 2. Dependencies should exist for the Component this BOM is describing, if one is set
390400
if self.metadata.component and not self.metadata.component.dependencies:
391401
warnings.warn(
392-
f'The Component this BOM is describing {self.metadata.component.purl} has no defined dependencies'
393-
f'which means the Dependency Graph is incomplete - you should add direct dependencies to this Component'
394-
f'to complete the Dependency Graph data.',
402+
f'The Component this BOM is describing (PURL={self.metadata.component.purl}) has no defined '
403+
f'dependencies which means the Dependency Graph is incomplete - you should add direct dependencies to '
404+
f'this Component to complete the Dependency Graph data.',
395405
UserWarning
396406
)
397407

cyclonedx/model/component.py

+11-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
import warnings
2121
from enum import Enum
2222
from os.path import exists
23-
from typing import Any, Iterable, Optional
23+
from typing import Any, Iterable, Optional, Set
2424

2525
# See https://github.com/package-url/packageurl-python/issues/65
2626
from packageurl import PackageURL # type: ignore
@@ -1159,6 +1159,16 @@ def has_vulnerabilities(self) -> bool:
11591159
"""
11601160
return bool(self.get_vulnerabilities())
11611161

1162+
def get_all_nested_components(self, include_self: bool = False) -> Set["Component"]:
1163+
components = set()
1164+
if include_self:
1165+
components.add(self)
1166+
1167+
for c in self.components:
1168+
components.update(c.get_all_nested_components(include_self=True))
1169+
1170+
return components
1171+
11621172
def get_pypi_url(self) -> str:
11631173
if self.version:
11641174
return f'https://pypi.org/project/{self.name}/{self.version}'

cyclonedx/output/json.py

+61-51
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,9 @@ def _specialise_output_for_schema_version(self, bom_json: Dict[Any, Any]) -> str
115115
del bom_json['metadata']['properties']
116116

117117
# Iterate Components
118+
if self.get_bom().metadata.component:
119+
bom_json['metadata'] = self._recurse_specialise_component(bom_json=bom_json['metadata'],
120+
base_key='component')
118121
bom_json = self._recurse_specialise_component(bom_json=bom_json)
119122

120123
# Iterate Services
@@ -155,60 +158,67 @@ def _get_schema_uri(self) -> Optional[str]:
155158

156159
def _recurse_specialise_component(self, bom_json: Dict[Any, Any], base_key: str = 'components') -> Dict[Any, Any]:
157160
if base_key in bom_json.keys():
158-
for i in range(len(bom_json[base_key])):
159-
if not self.component_supports_mime_type_attribute() \
160-
and 'mime-type' in bom_json[base_key][i].keys():
161-
del bom_json[base_key][i]['mime-type']
162-
163-
if not self.component_supports_supplier() and 'supplier' in bom_json[base_key][i].keys():
164-
del bom_json[base_key][i]['supplier']
165-
166-
if not self.component_supports_author() and 'author' in bom_json[base_key][i].keys():
167-
del bom_json[base_key][i]['author']
168-
169-
if self.component_version_optional() and 'version' in bom_json[base_key][i] \
170-
and bom_json[base_key][i].get('version', '') == "":
171-
del bom_json[base_key][i]['version']
172-
173-
if not self.component_supports_pedigree() and 'pedigree' in bom_json[base_key][i].keys():
174-
del bom_json[base_key][i]['pedigree']
175-
elif 'pedigree' in bom_json[base_key][i].keys():
176-
if 'ancestors' in bom_json[base_key][i]['pedigree'].keys():
177-
# recurse into ancestors
178-
bom_json[base_key][i]['pedigree'] = self._recurse_specialise_component(
179-
bom_json=bom_json[base_key][i]['pedigree'], base_key='ancestors'
180-
)
181-
if 'descendants' in bom_json[base_key][i]['pedigree'].keys():
182-
# recurse into descendants
183-
bom_json[base_key][i]['pedigree'] = self._recurse_specialise_component(
184-
bom_json=bom_json[base_key][i]['pedigree'], base_key='descendants'
185-
)
186-
if 'variants' in bom_json[base_key][i]['pedigree'].keys():
187-
# recurse into variants
188-
bom_json[base_key][i]['pedigree'] = self._recurse_specialise_component(
189-
bom_json=bom_json[base_key][i]['pedigree'], base_key='variants'
190-
)
191-
192-
if not self.external_references_supports_hashes() and 'externalReferences' \
193-
in bom_json[base_key][i].keys():
194-
for j in range(len(bom_json[base_key][i]['externalReferences'])):
195-
del bom_json[base_key][i]['externalReferences'][j]['hashes']
196-
197-
if not self.component_supports_properties() and 'properties' in bom_json[base_key][i].keys():
198-
del bom_json[base_key][i]['properties']
199-
200-
# recurse
201-
if 'components' in bom_json[base_key][i].keys():
202-
bom_json[base_key][i] = self._recurse_specialise_component(bom_json=bom_json[base_key][i])
203-
204-
if not self.component_supports_evidence() and 'evidence' in bom_json[base_key][i].keys():
205-
del bom_json[base_key][i]['evidence']
206-
207-
if not self.component_supports_release_notes() and 'releaseNotes' in bom_json[base_key][i].keys():
208-
del bom_json[base_key][i]['releaseNotes']
161+
if isinstance(bom_json[base_key], dict):
162+
bom_json[base_key] = self._specialise_component_data(component_json=bom_json[base_key])
163+
else:
164+
for i in range(len(bom_json[base_key])):
165+
bom_json[base_key][i] = self._specialise_component_data(component_json=bom_json[base_key][i])
209166

210167
return bom_json
211168

169+
def _specialise_component_data(self, component_json: Dict[Any, Any]) -> Dict[Any, Any]:
170+
if not self.component_supports_mime_type_attribute() and 'mime-type' in component_json.keys():
171+
del component_json['mime-type']
172+
173+
if not self.component_supports_supplier() and 'supplier' in component_json.keys():
174+
del component_json['supplier']
175+
176+
if not self.component_supports_author() and 'author' in component_json.keys():
177+
del component_json['author']
178+
179+
if self.component_version_optional() and 'version' in component_json \
180+
and component_json.get('version', '') == "":
181+
del component_json['version']
182+
183+
if not self.component_supports_pedigree() and 'pedigree' in component_json.keys():
184+
del component_json['pedigree']
185+
elif 'pedigree' in component_json.keys():
186+
if 'ancestors' in component_json['pedigree'].keys():
187+
# recurse into ancestors
188+
component_json['pedigree'] = self._recurse_specialise_component(
189+
bom_json=component_json['pedigree'], base_key='ancestors'
190+
)
191+
if 'descendants' in component_json['pedigree'].keys():
192+
# recurse into descendants
193+
component_json['pedigree'] = self._recurse_specialise_component(
194+
bom_json=component_json['pedigree'], base_key='descendants'
195+
)
196+
if 'variants' in component_json['pedigree'].keys():
197+
# recurse into variants
198+
component_json['pedigree'] = self._recurse_specialise_component(
199+
bom_json=component_json['pedigree'], base_key='variants'
200+
)
201+
202+
if not self.external_references_supports_hashes() and 'externalReferences' \
203+
in component_json.keys():
204+
for j in range(len(component_json['externalReferences'])):
205+
del component_json['externalReferences'][j]['hashes']
206+
207+
if not self.component_supports_properties() and 'properties' in component_json.keys():
208+
del component_json['properties']
209+
210+
# recurse
211+
if 'components' in component_json.keys():
212+
component_json = self._recurse_specialise_component(bom_json=component_json)
213+
214+
if not self.component_supports_evidence() and 'evidence' in component_json.keys():
215+
del component_json['evidence']
216+
217+
if not self.component_supports_release_notes() and 'releaseNotes' in component_json.keys():
218+
del component_json['releaseNotes']
219+
220+
return component_json
221+
212222

213223
class JsonV1Dot0(Json, SchemaVersion1Dot0):
214224

0 commit comments

Comments
 (0)