Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 11 commits into from
Aug 1, 2022
Merged
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"]:
madpah marked this conversation as resolved.
Show resolved Hide resolved
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