Source code

Revision control

Copy as Markdown

Other Tools

# This file is part of CycloneDX Python Library
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
# Copyright (c) OWASP Foundation. All Rights Reserved.
"""Bom related utilities"""
__all__ = [
'BomRefDiscriminator',
'BomDependencyGraphFlatMerger',
]
from collections.abc import Iterable
from itertools import chain
from random import random
from typing import TYPE_CHECKING, Any
from ...model.dependency import Dependency
if TYPE_CHECKING: # pragma: no cover
from ...model.bom import Bom
from ...model.bom_ref import BomRef
class BomRefDiscriminator:
"""
Ensure that a collection of BomRef objects
has unique, non‑empty :attr:`cyclonedx.model.bom_ref.BomRef.value`.
The discriminator inspects each provided BomRef and assigns a newly
generated identifier to any instance whose ``value`` is missing or
duplicates an earlier one.
All original values are preserved and can be restored via :meth:`reset()`
or by using this class as a context manager.
"""
def __init__(self, bomrefs: Iterable['BomRef'], prefix: str = 'BomRef') -> None:
# NOTE: do not use dict/set here, different BomRefs with same value
# have same hash and would shadow each other.
self._bomrefs = tuple((bomref, bomref.value) for bomref in bomrefs)
self._prefix = prefix
def __enter__(self) -> None:
self.discriminate()
def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
self.reset()
def discriminate(self) -> None:
"""
Enforce uniqueness across all
:attr:`cyclonedx.model.bom_ref.BomRef.value`s.
Any BomRef whose ``value`` is ``None`` or duplicates a previously
encountered value is assigned a newly generated unique identifier.
"""
known_values = []
for bomref, _ in self._bomrefs:
value = bomref.value
if value is None or value in known_values:
value = self._make_unique()
bomref.value = value
known_values.append(value)
def reset(self) -> None:
"""
Restore all :attr:`cyclonedx.model.bom_ref.BomRef.value`s to
their original state.
"""
for bomref, original_value in self._bomrefs:
bomref.value = original_value
def _make_unique(self) -> str:
return f'{self._prefix}{str(random())[1:]}{str(random())[1:]}' # nosec B311
@classmethod
def from_bom(cls, bom: 'Bom', prefix: str = 'BomRef') -> 'BomRefDiscriminator':
"""
Create a discriminator for all :class:`cyclonedx.model.bom_ref.BomRefs`
contained within a Bom.
This includes BomRefs from
* :attr:`cyclonedx.model.bom.Bom.components`
* :attr:`cyclonedx.model.bom.Bom.services`
* :attr:`cyclonedx.model.bom.Bom.vulnerabilities`
"""
return cls(chain(
(c.bom_ref for c in bom._get_all_components()),
(s.bom_ref for s in bom.services),
(v.bom_ref for v in bom.vulnerabilities),
), prefix)
class BomDependencyGraphFlatMerger:
"""
Context‑manager utility that temporarily flattens and merges all
:attr:`cyclonedx.model.bom.Bom.dependencies`.
When used as a context manager, the :class:`cyclonedx.model.bom.Bom`'s
dependency graph is replaced with a flattened, merged representation
for the duration of the ``with`` block and automatically restored
afterward.
"""
def __init__(self, bom: 'Bom') -> None:
self._bom = bom
# NOTE: do not use the getter - see `reset()` for reasons.
self._deps = self._bom._dependencies
def __enter__(self) -> None:
self.flatten_merge()
def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
self.reset()
def flatten_merge(self) -> None:
"""
Flatten and merge all :attr:`cyclonedx.model.bom.Bom.dependencies`.
This produces a non‑recursive, merged representation of the entire
dependency graph and assigns it to the Bom.
.. note::
The original dependency graph is not modified. A new, flattened
dependency structure is assigned to the Bom.
"""
self._bom.dependencies = self._flatten_merge(self._deps)
def reset(self) -> None:
"""
Restore the :class:`cyclonedx.model.bom.Bom`'s dependency graph to
its original state.
.. note::
This does not modify the dependency graph. It simply reassigns
the original dependency collection back to the Bom.
"""
# NOTE: not using the setter, which would create overhead,
# and - most importantly - this could cause deduplication of an existing malformed set.
# Just access the internal field directly!
self._bom._dependencies = self._deps
@staticmethod
def _flatten_merge(deps: Iterable[Dependency]) -> Iterable[Dependency]:
flat: dict['BomRef', list['BomRef']] = {}
todos = list(deps)
seen = set()
while todos:
todo = todos.pop()
if (todo_id := id(todo)) in seen:
continue
seen.add(todo_id)
ds = flat.setdefault(todo.ref, [])
if todo_deps := todo.dependencies:
ds.extend(d.ref for d in todo_deps)
todos.extend(todo_deps)
return (
Dependency(br, (Dependency(d) for d in ds))
for br, ds
in flat.items()
)