Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 32 additions & 27 deletions cyclonedx/model/bom.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
from collections.abc import Generator, Iterable
from datetime import datetime
from enum import Enum
from itertools import chain
from typing import TYPE_CHECKING, Optional, Union
from uuid import UUID, uuid4
from warnings import warn
Expand Down Expand Up @@ -804,7 +803,7 @@ def urn(self) -> str:
# idea: have 'serial_number' be a string, and use it instead of this method
return f'{_BOM_LINK_PREFIX}{self.serial_number}/{self.version}'

def validate(self) -> bool:
def validate(self) -> bool: # noqa: C901
"""
Perform data-model level validations to make sure we have some known data integrity prior to attempting output
of this `Bom`
Expand All @@ -817,20 +816,25 @@ def validate(self) -> bool:
"""
# !! deprecated function. have this as an part of the normalization process, like the BomRefDiscrimator
# 0. Make sure all Dependable have a Dependency entry
if self.metadata.component:
# Cache existing refs to avoid registering dependencies unnecessarily
existing_refs = {d.ref for d in self.dependencies}

if self.metadata.component and self.metadata.component.bom_ref not in existing_refs:
self.register_dependency(target=self.metadata.component)
for _c in self.components:
self.register_dependency(target=_c)
if _c.bom_ref not in existing_refs:
self.register_dependency(target=_c)
for _s in self.services:
self.register_dependency(target=_s)
if _s.bom_ref not in existing_refs:
self.register_dependency(target=_s)

# 1. Make sure dependencies are all in this Bom.
component_bom_refs = set(map(lambda c: c.bom_ref, self._get_all_components())) | set(
map(lambda s: s.bom_ref, self.services))
dependency_bom_refs = set(chain(
(d.ref for d in self.dependencies),
chain.from_iterable(d.dependencies_as_bom_refs() for d in self.dependencies)
))
component_bom_refs = {c.bom_ref for c in self._get_all_components()}
component_bom_refs.update(s.bom_ref for s in self.services)
dependency_bom_refs = set()
for d in self.dependencies:
dependency_bom_refs.add(d.ref)
dependency_bom_refs.update(d.dependencies_as_bom_refs())
dependency_diff = dependency_bom_refs - component_bom_refs
if len(dependency_diff) > 0:
raise UnknownComponentDependencyException(
Expand All @@ -839,26 +843,27 @@ def validate(self) -> bool:

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

# 3. If a LicenseExpression is set, then there must be no other license.
# see https://github.com/CycloneDX/specification/pull/205
elems: list[Union[BomMetaData, Component, Service]] = [self.metadata]
if self.metadata.component:
elems.extend(self.metadata.component.get_all_nested_components(include_self=True))
for c in self.components:
elems.extend(c.get_all_nested_components(include_self=True))
elems.extend(self.services)

elem: Union[BomMetaData, Component, Service]
for elem in chain( # type:ignore[assignment]
[self.metadata],
self.metadata.component.get_all_nested_components(include_self=True) if self.metadata.component else [],
chain.from_iterable(c.get_all_nested_components(include_self=True) for c in self.components),
self.services
):
for elem in elems:
if len(elem.licenses) > 1 and any(isinstance(li, LicenseExpression) for li in elem.licenses):
raise LicenseExpressionAlongWithOthersException(
f'Found LicenseExpression along with others licenses in: {elem!r}')
Expand Down