Skip to content

Commit

Permalink
Merge pull request #276 from CycloneDX/fix/bom-validation-nested-comp…
Browse files Browse the repository at this point in the history
…onents-isue-275

fix: BOM validation fails when Components or Services are nested #275 

fix: updated dependencies #271, #270, #269 and #256
  • Loading branch information
madpah committed Aug 1, 2022
2 parents 01cb53b + 6caee65 commit 68a0cdd
Show file tree
Hide file tree
Showing 32 changed files with 2,187 additions and 636 deletions.
5 changes: 4 additions & 1 deletion .isort.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
## read the docs: https://pycqa.github.io/isort/docs/configuration/options.html
## keep in sync with flake8 config - in `tox.ini` file
known_first_party = cyclonedx
skip_gitignore = true
skip_gitignore = false
skip_glob =
build/*,dist/*,__pycache__,.eggs,*.egg-info*,
*_cache,*.cache,
Expand All @@ -15,3 +15,6 @@ ensure_newline_before_comments = true
include_trailing_comma = true
line_length = 120
multi_line_output = 3
src_paths =
cyclonedx
tests
14 changes: 7 additions & 7 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@ repos:
entry: poetry run tox -e mypy
pass_filenames: false
language: system
# - repo: local
# hooks:
# - id: system
# name: isort
# entry: poetry run isort
# pass_filenames: false
# language: system
- repo: local
hooks:
- id: system
name: isort
entry: poetry run isort -c .
pass_filenames: false
language: system
- repo: local
hooks:
- id: system
Expand Down
22 changes: 16 additions & 6 deletions cyclonedx/model/bom.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
# Copyright (c) OWASP Foundation. All Rights Reserved.
import warnings
from datetime import datetime, timezone
from typing import Iterable, Optional
from typing import Iterable, Optional, Set
from uuid import UUID, uuid4

from sortedcontainers import SortedSet
Expand Down Expand Up @@ -356,6 +356,16 @@ def external_references(self) -> "SortedSet[ExternalReference]":
def external_references(self, external_references: Iterable[ExternalReference]) -> None:
self._external_references = SortedSet(external_references)

def _get_all_components(self) -> Set[Component]:
components: Set[Component] = set()
if self.metadata.component:
components.update(self.metadata.component.get_all_nested_components(include_self=True))

for c in self.components:
components.update(c.get_all_nested_components(include_self=True))

return components

def has_vulnerabilities(self) -> bool:
"""
Check whether this Bom has any declared vulnerabilities.
Expand All @@ -376,8 +386,8 @@ def validate(self) -> bool:
"""

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

all_dependency_bom_refs = set().union(*(c.dependencies for c in self.components))
dependency_diff = all_dependency_bom_refs - all_bom_refs
Expand All @@ -389,9 +399,9 @@ def validate(self) -> bool:
# 2. Dependencies should exist for the Component this BOM is describing, if one is set
if self.metadata.component and not self.metadata.component.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 Component'
f'to complete the Dependency Graph data.',
f'The Component this BOM is describing (PURL={self.metadata.component.purl}) has no defined '
f'dependencies which means the Dependency Graph is incomplete - you should add direct dependencies to '
f'this Component to complete the Dependency Graph data.',
UserWarning
)

Expand Down
12 changes: 11 additions & 1 deletion cyclonedx/model/component.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
import warnings
from enum import Enum
from os.path import exists
from typing import Any, Iterable, Optional
from typing import Any, Iterable, Optional, Set

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

def get_all_nested_components(self, include_self: bool = False) -> Set["Component"]:
components = set()
if include_self:
components.add(self)

for c in self.components:
components.update(c.get_all_nested_components(include_self=True))

return components

def get_pypi_url(self) -> str:
if self.version:
return f'https://pypi.org/project/{self.name}/{self.version}'
Expand Down
112 changes: 61 additions & 51 deletions cyclonedx/output/json.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,9 @@ def _specialise_output_for_schema_version(self, bom_json: Dict[Any, Any]) -> str
del bom_json['metadata']['properties']

# Iterate Components
if self.get_bom().metadata.component:
bom_json['metadata'] = self._recurse_specialise_component(bom_json=bom_json['metadata'],
base_key='component')
bom_json = self._recurse_specialise_component(bom_json=bom_json)

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

def _recurse_specialise_component(self, bom_json: Dict[Any, Any], base_key: str = 'components') -> Dict[Any, Any]:
if base_key in bom_json.keys():
for i in range(len(bom_json[base_key])):
if not self.component_supports_mime_type_attribute() \
and 'mime-type' in bom_json[base_key][i].keys():
del bom_json[base_key][i]['mime-type']

if not self.component_supports_supplier() and 'supplier' in bom_json[base_key][i].keys():
del bom_json[base_key][i]['supplier']

if not self.component_supports_author() and 'author' in bom_json[base_key][i].keys():
del bom_json[base_key][i]['author']

if self.component_version_optional() and 'version' in bom_json[base_key][i] \
and bom_json[base_key][i].get('version', '') == "":
del bom_json[base_key][i]['version']

if not self.component_supports_pedigree() and 'pedigree' in bom_json[base_key][i].keys():
del bom_json[base_key][i]['pedigree']
elif 'pedigree' in bom_json[base_key][i].keys():
if 'ancestors' in bom_json[base_key][i]['pedigree'].keys():
# recurse into ancestors
bom_json[base_key][i]['pedigree'] = self._recurse_specialise_component(
bom_json=bom_json[base_key][i]['pedigree'], base_key='ancestors'
)
if 'descendants' in bom_json[base_key][i]['pedigree'].keys():
# recurse into descendants
bom_json[base_key][i]['pedigree'] = self._recurse_specialise_component(
bom_json=bom_json[base_key][i]['pedigree'], base_key='descendants'
)
if 'variants' in bom_json[base_key][i]['pedigree'].keys():
# recurse into variants
bom_json[base_key][i]['pedigree'] = self._recurse_specialise_component(
bom_json=bom_json[base_key][i]['pedigree'], base_key='variants'
)

if not self.external_references_supports_hashes() and 'externalReferences' \
in bom_json[base_key][i].keys():
for j in range(len(bom_json[base_key][i]['externalReferences'])):
del bom_json[base_key][i]['externalReferences'][j]['hashes']

if not self.component_supports_properties() and 'properties' in bom_json[base_key][i].keys():
del bom_json[base_key][i]['properties']

# recurse
if 'components' in bom_json[base_key][i].keys():
bom_json[base_key][i] = self._recurse_specialise_component(bom_json=bom_json[base_key][i])

if not self.component_supports_evidence() and 'evidence' in bom_json[base_key][i].keys():
del bom_json[base_key][i]['evidence']

if not self.component_supports_release_notes() and 'releaseNotes' in bom_json[base_key][i].keys():
del bom_json[base_key][i]['releaseNotes']
if isinstance(bom_json[base_key], dict):
bom_json[base_key] = self._specialise_component_data(component_json=bom_json[base_key])
else:
for i in range(len(bom_json[base_key])):
bom_json[base_key][i] = self._specialise_component_data(component_json=bom_json[base_key][i])

return bom_json

def _specialise_component_data(self, component_json: Dict[Any, Any]) -> Dict[Any, Any]:
if not self.component_supports_mime_type_attribute() and 'mime-type' in component_json.keys():
del component_json['mime-type']

if not self.component_supports_supplier() and 'supplier' in component_json.keys():
del component_json['supplier']

if not self.component_supports_author() and 'author' in component_json.keys():
del component_json['author']

if self.component_version_optional() and 'version' in component_json \
and component_json.get('version', '') == "":
del component_json['version']

if not self.component_supports_pedigree() and 'pedigree' in component_json.keys():
del component_json['pedigree']
elif 'pedigree' in component_json.keys():
if 'ancestors' in component_json['pedigree'].keys():
# recurse into ancestors
component_json['pedigree'] = self._recurse_specialise_component(
bom_json=component_json['pedigree'], base_key='ancestors'
)
if 'descendants' in component_json['pedigree'].keys():
# recurse into descendants
component_json['pedigree'] = self._recurse_specialise_component(
bom_json=component_json['pedigree'], base_key='descendants'
)
if 'variants' in component_json['pedigree'].keys():
# recurse into variants
component_json['pedigree'] = self._recurse_specialise_component(
bom_json=component_json['pedigree'], base_key='variants'
)

if not self.external_references_supports_hashes() and 'externalReferences' \
in component_json.keys():
for j in range(len(component_json['externalReferences'])):
del component_json['externalReferences'][j]['hashes']

if not self.component_supports_properties() and 'properties' in component_json.keys():
del component_json['properties']

# recurse
if 'components' in component_json.keys():
component_json = self._recurse_specialise_component(bom_json=component_json)

if not self.component_supports_evidence() and 'evidence' in component_json.keys():
del component_json['evidence']

if not self.component_supports_release_notes() and 'releaseNotes' in component_json.keys():
del component_json['releaseNotes']

return component_json


class JsonV1Dot0(Json, SchemaVersion1Dot0):

Expand Down
Loading

0 comments on commit 68a0cdd

Please sign in to comment.