This commit is contained in:
Iliyan Angelov
2025-12-01 06:50:10 +02:00
parent 91f51bc6fe
commit 62c1fe5951
4682 changed files with 544807 additions and 31208 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,748 @@
# 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
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# 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.
from datetime import datetime
from itertools import chain
from typing import TYPE_CHECKING, Generator, Iterable, Optional, Union
from uuid import UUID, uuid4
from warnings import warn
import py_serializable as serializable
from sortedcontainers import SortedSet
from .._internal.compare import ComparableTuple as _ComparableTuple
from .._internal.time import get_now_utc as _get_now_utc
from ..exception.model import LicenseExpressionAlongWithOthersException, UnknownComponentDependencyException
from ..schema.schema import (
SchemaVersion1Dot0,
SchemaVersion1Dot1,
SchemaVersion1Dot2,
SchemaVersion1Dot3,
SchemaVersion1Dot4,
SchemaVersion1Dot5,
SchemaVersion1Dot6,
)
from ..serialization import UrnUuidHelper
from . import _BOM_LINK_PREFIX, ExternalReference, Property
from .bom_ref import BomRef
from .component import Component
from .contact import OrganizationalContact, OrganizationalEntity
from .definition import Definitions
from .dependency import Dependable, Dependency
from .license import License, LicenseExpression, LicenseRepository, _LicenseRepositorySerializationHelper
from .lifecycle import Lifecycle, LifecycleRepository, _LifecycleRepositoryHelper
from .service import Service
from .tool import Tool, ToolRepository, _ToolRepositoryHelper
from .vulnerability import Vulnerability
if TYPE_CHECKING: # pragma: no cover
from packageurl import PackageURL
@serializable.serializable_class
class BomMetaData:
"""
This is our internal representation of the metadata complex type within the CycloneDX standard.
.. note::
See the CycloneDX Schema for Bom metadata: https://cyclonedx.org/docs/1.6/#type_metadata
"""
def __init__(
self, *,
tools: Optional[Union[Iterable[Tool], ToolRepository]] = None,
authors: Optional[Iterable[OrganizationalContact]] = None,
component: Optional[Component] = None,
supplier: Optional[OrganizationalEntity] = None,
licenses: Optional[Iterable[License]] = None,
properties: Optional[Iterable[Property]] = None,
timestamp: Optional[datetime] = None,
manufacturer: Optional[OrganizationalEntity] = None,
lifecycles: Optional[Iterable[Lifecycle]] = None,
# Deprecated as of v1.6
manufacture: Optional[OrganizationalEntity] = None,
) -> None:
self.timestamp = timestamp or _get_now_utc()
self.tools = tools or [] # type:ignore[assignment]
self.authors = authors or [] # type:ignore[assignment]
self.component = component
self.supplier = supplier
self.licenses = licenses or [] # type:ignore[assignment]
self.properties = properties or [] # type:ignore[assignment]
self.manufacturer = manufacturer
self.lifecycles = lifecycles or [] # type:ignore[assignment]
self.manufacture = manufacture
if manufacture:
warn(
'`bom.metadata.manufacture` is deprecated from CycloneDX v1.6 onwards. '
'Please use `bom.metadata.component.manufacturer` instead.',
DeprecationWarning)
@property
@serializable.type_mapping(serializable.helpers.XsdDateTime)
@serializable.xml_sequence(1)
def timestamp(self) -> datetime:
"""
The date and time (in UTC) when this BomMetaData was created.
Returns:
`datetime` instance in UTC timezone
"""
return self._timestamp
@timestamp.setter
def timestamp(self, timestamp: datetime) -> None:
self._timestamp = timestamp
@property
@serializable.view(SchemaVersion1Dot5)
@serializable.view(SchemaVersion1Dot6)
@serializable.type_mapping(_LifecycleRepositoryHelper)
@serializable.xml_sequence(2)
def lifecycles(self) -> LifecycleRepository:
"""
An optional list of BOM lifecycle stages.
Returns:
Set of `Lifecycle`
"""
return self._lifecycles
@lifecycles.setter
def lifecycles(self, lifecycles: Iterable[Lifecycle]) -> None:
self._lifecycles = LifecycleRepository(lifecycles)
@property
@serializable.type_mapping(_ToolRepositoryHelper)
@serializable.xml_sequence(3)
def tools(self) -> ToolRepository:
"""
Tools used to create this BOM.
Returns:
:class:`ToolRepository` object.
"""
return self._tools
@tools.setter
def tools(self, tools: Union[Iterable[Tool], ToolRepository]) -> None:
self._tools = tools \
if isinstance(tools, ToolRepository) \
else ToolRepository(tools=tools)
@property
@serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'author')
@serializable.xml_sequence(4)
def authors(self) -> 'SortedSet[OrganizationalContact]':
"""
The person(s) who created the BOM.
Authors are common in BOMs created through manual processes.
BOMs created through automated means may not have authors.
Returns:
Set of `OrganizationalContact`
"""
return self._authors
@authors.setter
def authors(self, authors: Iterable[OrganizationalContact]) -> None:
self._authors = SortedSet(authors)
@property
@serializable.xml_sequence(5)
def component(self) -> Optional[Component]:
"""
The (optional) component that the BOM describes.
Returns:
`cyclonedx.model.component.Component` instance for this Bom Metadata.
"""
return self._component
@component.setter
def component(self, component: Component) -> None:
"""
The (optional) component that the BOM describes.
Args:
component
`cyclonedx.model.component.Component` instance to add to this Bom Metadata.
Returns:
None
"""
self._component = component
@property
@serializable.view(SchemaVersion1Dot2)
@serializable.view(SchemaVersion1Dot3)
@serializable.view(SchemaVersion1Dot4)
@serializable.view(SchemaVersion1Dot5)
@serializable.view(SchemaVersion1Dot6)
@serializable.xml_sequence(6)
def manufacture(self) -> Optional[OrganizationalEntity]:
"""
The organization that manufactured the component that the BOM describes.
Returns:
`OrganizationalEntity` if set else `None`
"""
return self._manufacture
@manufacture.setter
def manufacture(self, manufacture: Optional[OrganizationalEntity]) -> None:
"""
@todo Based on https://github.com/CycloneDX/specification/issues/346,
we should set this data on `.component.manufacturer`.
"""
self._manufacture = manufacture
@property
@serializable.view(SchemaVersion1Dot6)
@serializable.xml_sequence(7)
def manufacturer(self) -> Optional[OrganizationalEntity]:
"""
The organization that created the BOM.
Manufacturer is common in BOMs created through automated processes. BOMs created through manual means may have
`@.authors` instead.
Returns:
`OrganizationalEntity` if set else `None`
"""
return self._manufacturer
@manufacturer.setter
def manufacturer(self, manufacturer: Optional[OrganizationalEntity]) -> None:
self._manufacturer = manufacturer
@property
@serializable.xml_sequence(8)
def supplier(self) -> Optional[OrganizationalEntity]:
"""
The organization that supplied the component that the BOM describes.
The supplier may often be the manufacturer, but may also be a distributor or repackager.
Returns:
`OrganizationalEntity` if set else `None`
"""
return self._supplier
@supplier.setter
def supplier(self, supplier: Optional[OrganizationalEntity]) -> None:
self._supplier = supplier
@property
@serializable.view(SchemaVersion1Dot3)
@serializable.view(SchemaVersion1Dot4)
@serializable.view(SchemaVersion1Dot5)
@serializable.view(SchemaVersion1Dot6)
@serializable.type_mapping(_LicenseRepositorySerializationHelper)
@serializable.xml_sequence(9)
def licenses(self) -> LicenseRepository:
"""
A optional list of statements about how this BOM is licensed.
Returns:
Set of `LicenseChoice`
"""
return self._licenses
@licenses.setter
def licenses(self, licenses: Iterable[License]) -> None:
self._licenses = LicenseRepository(licenses)
@property
@serializable.view(SchemaVersion1Dot3)
@serializable.view(SchemaVersion1Dot4)
@serializable.view(SchemaVersion1Dot5)
@serializable.view(SchemaVersion1Dot6)
@serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'property')
@serializable.xml_sequence(10)
def properties(self) -> 'SortedSet[Property]':
"""
Provides the ability to document properties in a key/value store. This provides flexibility to include data not
officially supported in the standard without having to use additional namespaces or create extensions.
Property names of interest to the general public are encouraged to be registered in the CycloneDX Property
Taxonomy - https://github.com/CycloneDX/cyclonedx-property-taxonomy. Formal registration is OPTIONAL.
Return:
Set of `Property`
"""
return self._properties
@properties.setter
def properties(self, properties: Iterable[Property]) -> None:
self._properties = SortedSet(properties)
def __comparable_tuple(self) -> _ComparableTuple:
return _ComparableTuple((
_ComparableTuple(self.authors), self.component, _ComparableTuple(self.licenses), self.manufacture,
_ComparableTuple(self.properties),
_ComparableTuple(self.lifecycles), self.supplier, self.timestamp, self.tools, self.manufacturer
))
def __eq__(self, other: object) -> bool:
if isinstance(other, BomMetaData):
return self.__comparable_tuple() == other.__comparable_tuple()
return False
def __hash__(self) -> int:
return hash(self.__comparable_tuple())
def __repr__(self) -> str:
return f'<BomMetaData timestamp={self.timestamp}, component={self.component}>'
@serializable.serializable_class(ignore_during_deserialization=['$schema', 'bom_format', 'spec_version'])
class Bom:
"""
This is our internal representation of a bill-of-materials (BOM).
Once you have an instance of `cyclonedx.model.bom.Bom`, you can pass this to an instance of
`cyclonedx.output.BaseOutput` to produce a CycloneDX document according to a specific schema version and format.
"""
def __init__(
self, *,
components: Optional[Iterable[Component]] = None,
services: Optional[Iterable[Service]] = None,
external_references: Optional[Iterable[ExternalReference]] = None,
serial_number: Optional[UUID] = None,
version: int = 1,
metadata: Optional[BomMetaData] = None,
dependencies: Optional[Iterable[Dependency]] = None,
vulnerabilities: Optional[Iterable[Vulnerability]] = None,
properties: Optional[Iterable[Property]] = None,
definitions: Optional[Definitions] = None,
) -> None:
"""
Create a new Bom that you can manually/programmatically add data to later.
Returns:
New, empty `cyclonedx.model.bom.Bom` instance.
"""
self.serial_number = serial_number or uuid4()
self.version = version
self.metadata = metadata or BomMetaData()
self.components = components or [] # type:ignore[assignment]
self.services = services or [] # type:ignore[assignment]
self.external_references = external_references or [] # type:ignore[assignment]
self.vulnerabilities = vulnerabilities or [] # type:ignore[assignment]
self.dependencies = dependencies or [] # type:ignore[assignment]
self.properties = properties or [] # type:ignore[assignment]
self.definitions = definitions or Definitions()
@property
@serializable.type_mapping(UrnUuidHelper)
@serializable.view(SchemaVersion1Dot1)
@serializable.view(SchemaVersion1Dot2)
@serializable.view(SchemaVersion1Dot3)
@serializable.view(SchemaVersion1Dot4)
@serializable.view(SchemaVersion1Dot5)
@serializable.view(SchemaVersion1Dot6)
@serializable.xml_attribute()
def serial_number(self) -> UUID:
"""
Unique UUID for this BOM
Returns:
`UUID` instance
`UUID` instance
"""
return self._serial_number
@serial_number.setter
def serial_number(self, serial_number: UUID) -> None:
self._serial_number = serial_number
@property
@serializable.xml_attribute()
def version(self) -> int:
return self._version
@version.setter
def version(self, version: int) -> None:
self._version = version
@property
@serializable.view(SchemaVersion1Dot2)
@serializable.view(SchemaVersion1Dot3)
@serializable.view(SchemaVersion1Dot4)
@serializable.view(SchemaVersion1Dot5)
@serializable.view(SchemaVersion1Dot6)
@serializable.xml_sequence(10)
def metadata(self) -> BomMetaData:
"""
Get our internal metadata object for this Bom.
Returns:
Metadata object instance for this Bom.
.. note::
See the CycloneDX Schema for Bom metadata: https://cyclonedx.org/docs/1.6/#type_metadata
"""
return self._metadata
@metadata.setter
def metadata(self, metadata: BomMetaData) -> None:
self._metadata = metadata
@property
@serializable.include_none(SchemaVersion1Dot0)
@serializable.include_none(SchemaVersion1Dot1)
@serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'component')
@serializable.xml_sequence(20)
def components(self) -> 'SortedSet[Component]':
"""
Get all the Components currently in this Bom.
Returns:
Set of `Component` in this Bom
"""
return self._components
@components.setter
def components(self, components: Iterable[Component]) -> None:
self._components = SortedSet(components)
@property
@serializable.view(SchemaVersion1Dot2)
@serializable.view(SchemaVersion1Dot3)
@serializable.view(SchemaVersion1Dot4)
@serializable.view(SchemaVersion1Dot5)
@serializable.view(SchemaVersion1Dot6)
@serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'service')
@serializable.xml_sequence(30)
def services(self) -> 'SortedSet[Service]':
"""
Get all the Services currently in this Bom.
Returns:
Set of `Service` in this BOM
"""
return self._services
@services.setter
def services(self, services: Iterable[Service]) -> None:
self._services = SortedSet(services)
@property
@serializable.view(SchemaVersion1Dot1)
@serializable.view(SchemaVersion1Dot2)
@serializable.view(SchemaVersion1Dot3)
@serializable.view(SchemaVersion1Dot4)
@serializable.view(SchemaVersion1Dot5)
@serializable.view(SchemaVersion1Dot6)
@serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'reference')
@serializable.xml_sequence(40)
def external_references(self) -> 'SortedSet[ExternalReference]':
"""
Provides the ability to document external references related to the BOM or to the project the BOM describes.
Returns:
Set of `ExternalReference`
"""
return self._external_references
@external_references.setter
def external_references(self, external_references: Iterable[ExternalReference]) -> None:
self._external_references = SortedSet(external_references)
@property
@serializable.view(SchemaVersion1Dot2)
@serializable.view(SchemaVersion1Dot3)
@serializable.view(SchemaVersion1Dot4)
@serializable.view(SchemaVersion1Dot5)
@serializable.view(SchemaVersion1Dot6)
@serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'dependency')
@serializable.xml_sequence(50)
def dependencies(self) -> 'SortedSet[Dependency]':
return self._dependencies
@dependencies.setter
def dependencies(self, dependencies: Iterable[Dependency]) -> None:
self._dependencies = SortedSet(dependencies)
# @property
# ...
# @serializable.view(SchemaVersion1Dot3)
# @serializable.view(SchemaVersion1Dot4)
# @serializable.view(SchemaVersion1Dot5)
# @serializable.xml_sequence(6)
# def compositions(self) -> ...:
# ... # TODO Since CDX 1.3
#
# @compositions.setter
# def compositions(self, ...) -> None:
# ... # TODO Since CDX 1.3
@property
# @serializable.view(SchemaVersion1Dot3) @todo: Update py-serializable to support view by OutputFormat filtering
# @serializable.view(SchemaVersion1Dot4) @todo: Update py-serializable to support view by OutputFormat filtering
@serializable.view(SchemaVersion1Dot5)
@serializable.view(SchemaVersion1Dot6)
@serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'property')
@serializable.xml_sequence(70)
def properties(self) -> 'SortedSet[Property]':
"""
Provides the ability to document properties in a name/value store. This provides flexibility to include data
not officially supported in the standard without having to use additional namespaces or create extensions.
Property names of interest to the general public are encouraged to be registered in the CycloneDX Property
Taxonomy - https://github.com/CycloneDX/cyclonedx-property-taxonomy. Formal registration is OPTIONAL.
Return:
Set of `Property`
"""
return self._properties
@properties.setter
def properties(self, properties: Iterable[Property]) -> None:
self._properties = SortedSet(properties)
@property
@serializable.view(SchemaVersion1Dot4)
@serializable.view(SchemaVersion1Dot5)
@serializable.view(SchemaVersion1Dot6)
@serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'vulnerability')
@serializable.xml_sequence(80)
def vulnerabilities(self) -> 'SortedSet[Vulnerability]':
"""
Get all the Vulnerabilities in this BOM.
Returns:
Set of `Vulnerability`
"""
return self._vulnerabilities
@vulnerabilities.setter
def vulnerabilities(self, vulnerabilities: Iterable[Vulnerability]) -> None:
self._vulnerabilities = SortedSet(vulnerabilities)
# @property
# ...
# @serializable.view(SchemaVersion1Dot5)
# @serializable.xml_sequence(9)
# def annotations(self) -> ...:
# ... # TODO Since CDX 1.5
#
# @annotations.setter
# def annotations(self, ...) -> None:
# ... # TODO Since CDX 1.5
# @property
# ...
# @serializable.view(SchemaVersion1Dot5)
# @formulation.xml_sequence(10)
# def formulation(self) -> ...:
# ... # TODO Since CDX 1.5
#
# @formulation.setter
# def formulation(self, ...) -> None:
# ... # TODO Since CDX 1.5
@property
@serializable.view(SchemaVersion1Dot6)
@serializable.xml_sequence(110)
def definitions(self) -> Optional[Definitions]:
"""
The repository for definitions
Returns:
`Definitions`
"""
return self._definitions if len(self._definitions.standards) > 0 else None
@definitions.setter
def definitions(self, definitions: Definitions) -> None:
self._definitions = definitions
def get_component_by_purl(self, purl: Optional['PackageURL']) -> Optional[Component]:
"""
Get a Component already in the Bom by its PURL
Args:
purl:
An instance of `packageurl.PackageURL` to look and find `Component`.
Returns:
`Component` or `None`
"""
if purl:
found = [x for x in self.components if x.purl == purl]
if len(found) == 1:
return found[0]
return None
def get_urn_uuid(self) -> str:
"""
Get the unique reference for this Bom.
Returns:
URN formatted UUID that uniquely identified this Bom instance.
"""
return self.serial_number.urn
def has_component(self, component: Component) -> bool:
"""
Check whether this Bom contains the provided Component.
Args:
component:
The instance of `cyclonedx.model.component.Component` to check if this Bom contains.
Returns:
`bool` - `True` if the supplied Component is part of this Bom, `False` otherwise.
"""
return component in self.components
def _get_all_components(self) -> Generator[Component, None, None]:
if self.metadata.component:
yield from self.metadata.component.get_all_nested_components(include_self=True)
for c in self.components:
yield from c.get_all_nested_components(include_self=True)
def get_vulnerabilities_for_bom_ref(self, bom_ref: BomRef) -> 'SortedSet[Vulnerability]':
"""
Get all known Vulnerabilities that affect the supplied bom_ref.
Args:
bom_ref: `BomRef`
Returns:
`SortedSet` of `Vulnerability`
"""
vulnerabilities: SortedSet[Vulnerability] = SortedSet()
for v in self.vulnerabilities:
for target in v.affects:
if target.ref == bom_ref.value:
vulnerabilities.add(v)
return vulnerabilities
def has_vulnerabilities(self) -> bool:
"""
Check whether this Bom has any declared vulnerabilities.
Returns:
`bool` - `True` if this Bom has at least one Vulnerability, `False` otherwise.
"""
return bool(self.vulnerabilities)
def register_dependency(self, target: Dependable, depends_on: Optional[Iterable[Dependable]] = None) -> None:
_d = next(filter(lambda _d: _d.ref == target.bom_ref, self.dependencies), None)
if _d:
# Dependency Target already registered - but it might have new dependencies to add
if depends_on:
_d.dependencies.update(map(lambda _d: Dependency(ref=_d.bom_ref), depends_on))
else:
# First time we are seeing this target as a Dependency
self._dependencies.add(Dependency(
ref=target.bom_ref,
dependencies=map(lambda _dep: Dependency(ref=_dep.bom_ref), depends_on) if depends_on else []
))
if depends_on:
# Ensure dependents are registered with no further dependents in the DependencyGraph
for _d2 in depends_on:
self.register_dependency(target=_d2, depends_on=None)
def urn(self) -> str:
return f'{_BOM_LINK_PREFIX}{self.serial_number}/{self.version}'
def validate(self) -> bool:
"""
Perform data-model level validations to make sure we have some known data integrity prior to attempting output
of this `Bom`
Returns:
`bool`
"""
# 0. Make sure all Dependable have a Dependency entry
if self.metadata.component:
self.register_dependency(target=self.metadata.component)
for _c in self.components:
self.register_dependency(target=_c)
for _s in self.services:
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)
))
dependency_diff = dependency_bom_refs - component_bom_refs
if len(dependency_diff) > 0:
raise UnknownComponentDependencyException(
'One or more Components have Dependency references to Components/Services that are not known in this '
f'BOM. They are: {dependency_diff}')
# 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
)
# 3. If a LicenseExpression is set, then there must be no other license.
# see https://github.com/CycloneDX/specification/pull/205
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
):
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}')
return True
def __comparable_tuple(self) -> _ComparableTuple:
return _ComparableTuple((
self.serial_number, self.version, self.metadata, _ComparableTuple(
self.components), _ComparableTuple(self.services),
_ComparableTuple(self.external_references), _ComparableTuple(
self.dependencies), _ComparableTuple(self.properties),
_ComparableTuple(self.vulnerabilities),
))
def __eq__(self, other: object) -> bool:
if isinstance(other, Bom):
return self.__comparable_tuple() == other.__comparable_tuple()
return False
def __hash__(self) -> int:
return hash(self.__comparable_tuple())
def __repr__(self) -> str:
return f'<Bom uuid={self.serial_number}, hash={hash(self)}>'

View File

@@ -0,0 +1,101 @@
# 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
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# 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.
from typing import TYPE_CHECKING, Any, Optional
import py_serializable as serializable
from ..exception.serialization import CycloneDxDeserializationException, SerializationOfUnexpectedValueException
if TYPE_CHECKING: # pragma: no cover
from typing import Type, TypeVar
_T_BR = TypeVar('_T_BR', bound='BomRef')
@serializable.serializable_class
class BomRef(serializable.helpers.BaseHelper):
"""
An identifier that can be used to reference objects elsewhere in the BOM.
This copies a similar pattern used in the CycloneDX PHP Library.
.. note::
See https://github.com/CycloneDX/cyclonedx-php-library/blob/master/docs/dev/decisions/BomDependencyDataModel.md
"""
def __init__(self, value: Optional[str] = None) -> None:
self.value = value
@property
@serializable.json_name('.')
@serializable.xml_name('.')
def value(self) -> Optional[str]:
return self._value
@value.setter
def value(self, value: Optional[str]) -> None:
# empty strings become `None`
self._value = value or None
def __eq__(self, other: object) -> bool:
return (self is other) or (
isinstance(other, BomRef)
# `None` value is not discriminative in this domain
# see also: `BomRefDiscriminator`
and other._value is not None
and self._value is not None
and other._value == self._value
)
def __lt__(self, other: Any) -> bool:
if isinstance(other, BomRef):
return str(self) < str(other)
return NotImplemented
def __hash__(self) -> int:
return hash(self._value or f'__id__{id(self)}')
def __repr__(self) -> str:
return f'<BomRef {self._value!r} id={id(self)}>'
def __str__(self) -> str:
return self._value or ''
def __bool__(self) -> bool:
return self._value is not None
# region impl BaseHelper
@classmethod
def serialize(cls, o: Any) -> Optional[str]:
if isinstance(o, cls):
return o.value
raise SerializationOfUnexpectedValueException(
f'Attempt to serialize a non-BomRef: {o!r}')
@classmethod
def deserialize(cls: 'Type[_T_BR]', o: Any) -> '_T_BR':
try:
return cls(value=str(o))
except ValueError as err:
raise CycloneDxDeserializationException(
f'BomRef string supplied does not parse: {o!r}'
) from err
# endregion impl BaseHelper

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,386 @@
# 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
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# 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.
from typing import Any, Iterable, Optional, Union
import py_serializable as serializable
from sortedcontainers import SortedSet
from .._internal.bom_ref import bom_ref_from_str as _bom_ref_from_str
from .._internal.compare import ComparableTuple as _ComparableTuple
from ..schema.schema import SchemaVersion1Dot6
from . import XsUri
from .bom_ref import BomRef
@serializable.serializable_class
class PostalAddress:
"""
This is our internal representation of the `postalAddressType` complex type that can be used in multiple places
within a CycloneDX BOM document.
.. note::
See the CycloneDX Schema definition: https://cyclonedx.org/docs/1.6/xml/#type_postalAddressType
"""
def __init__(
self, *,
bom_ref: Optional[Union[str, BomRef]] = None,
country: Optional[str] = None,
region: Optional[str] = None,
locality: Optional[str] = None,
post_office_box_number: Optional[str] = None,
postal_code: Optional[str] = None,
street_address: Optional[str] = None,
) -> None:
self._bom_ref = _bom_ref_from_str(bom_ref, optional=True)
self.country = country
self.region = region
self.locality = locality
self.post_office_box_number = post_office_box_number
self.postal_code = postal_code
self.street_address = street_address
@property
@serializable.json_name('bom-ref')
@serializable.type_mapping(BomRef)
@serializable.xml_attribute()
@serializable.xml_name('bom-ref')
def bom_ref(self) -> Optional[BomRef]:
"""
An optional identifier which can be used to reference the component elsewhere in the BOM. Every bom-ref MUST be
unique within the BOM.
Returns:
`BomRef`
"""
return self._bom_ref
@property
@serializable.xml_sequence(10)
def country(self) -> Optional[str]:
"""
The country name or the two-letter ISO 3166-1 country code.
Returns:
`str` or `None`
"""
return self._country
@country.setter
def country(self, country: Optional[str]) -> None:
self._country = country
@property
@serializable.xml_sequence(20)
def region(self) -> Optional[str]:
"""
The region or state in the country. For example, Texas.
Returns:
`str` or `None`
"""
return self._region
@region.setter
def region(self, region: Optional[str]) -> None:
self._region = region
@property
@serializable.xml_sequence(30)
def locality(self) -> Optional[str]:
"""
The locality or city within the country. For example, Austin.
Returns:
`str` or `None`
"""
return self._locality
@locality.setter
def locality(self, locality: Optional[str]) -> None:
self._locality = locality
@property
@serializable.xml_sequence(40)
def post_office_box_number(self) -> Optional[str]:
"""
The post office box number. For example, 901.
Returns:
`str` or `None`
"""
return self._post_office_box_number
@post_office_box_number.setter
def post_office_box_number(self, post_office_box_number: Optional[str]) -> None:
self._post_office_box_number = post_office_box_number
@property
@serializable.xml_sequence(60)
def postal_code(self) -> Optional[str]:
"""
The postal code. For example, 78758.
Returns:
`str` or `None`
"""
return self._postal_code
@postal_code.setter
def postal_code(self, postal_code: Optional[str]) -> None:
self._postal_code = postal_code
@property
@serializable.xml_sequence(70)
def street_address(self) -> Optional[str]:
"""
The street address. For example, 100 Main Street.
Returns:
`str` or `None`
"""
return self._street_address
@street_address.setter
def street_address(self, street_address: Optional[str]) -> None:
self._street_address = street_address
def __comparable_tuple(self) -> _ComparableTuple:
return _ComparableTuple((
self.country, self.region, self.locality, self.postal_code,
self.post_office_box_number,
self.street_address,
None if self.bom_ref is None else self.bom_ref.value,
))
def __eq__(self, other: object) -> bool:
if isinstance(other, PostalAddress):
return self.__comparable_tuple() == other.__comparable_tuple()
return False
def __lt__(self, other: Any) -> bool:
if isinstance(other, PostalAddress):
return self.__comparable_tuple() < other.__comparable_tuple()
return NotImplemented
def __hash__(self) -> int:
return hash(self.__comparable_tuple())
def __repr__(self) -> str:
return f'<PostalAddress bom-ref={self.bom_ref}, street_address={self.street_address}, country={self.country}>'
@serializable.serializable_class
class OrganizationalContact:
"""
This is our internal representation of the `organizationalContact` complex type that can be used in multiple places
within a CycloneDX BOM document.
.. note::
See the CycloneDX Schema definition: https://cyclonedx.org/docs/1.6/xml/#type_organizationalContact
"""
def __init__(
self, *,
name: Optional[str] = None,
phone: Optional[str] = None,
email: Optional[str] = None,
) -> None:
self.name = name
self.email = email
self.phone = phone
@property
@serializable.xml_sequence(1)
@serializable.xml_string(serializable.XmlStringSerializationType.NORMALIZED_STRING)
def name(self) -> Optional[str]:
"""
Get the name of the contact.
Returns:
`str` if set else `None`
"""
return self._name
@name.setter
def name(self, name: Optional[str]) -> None:
self._name = name
@property
@serializable.xml_sequence(2)
@serializable.xml_string(serializable.XmlStringSerializationType.NORMALIZED_STRING)
def email(self) -> Optional[str]:
"""
Get the email of the contact.
Returns:
`str` if set else `None`
"""
return self._email
@email.setter
def email(self, email: Optional[str]) -> None:
self._email = email
@property
@serializable.xml_sequence(3)
@serializable.xml_string(serializable.XmlStringSerializationType.NORMALIZED_STRING)
def phone(self) -> Optional[str]:
"""
Get the phone of the contact.
Returns:
`str` if set else `None`
"""
return self._phone
@phone.setter
def phone(self, phone: Optional[str]) -> None:
self._phone = phone
def __comparable_tuple(self) -> _ComparableTuple:
return _ComparableTuple((
self.name, self.email, self.phone
))
def __eq__(self, other: object) -> bool:
if isinstance(other, OrganizationalContact):
return self.__comparable_tuple() == other.__comparable_tuple()
return False
def __lt__(self, other: Any) -> bool:
if isinstance(other, OrganizationalContact):
return self.__comparable_tuple() < other.__comparable_tuple()
return NotImplemented
def __hash__(self) -> int:
return hash(self.__comparable_tuple())
def __repr__(self) -> str:
return f'<OrganizationalContact name={self.name}, email={self.email}, phone={self.phone}>'
@serializable.serializable_class
class OrganizationalEntity:
"""
This is our internal representation of the `organizationalEntity` complex type that can be used in multiple places
within a CycloneDX BOM document.
.. note::
See the CycloneDX Schema definition: https://cyclonedx.org/docs/1.6/xml/#type_organizationalEntity
"""
def __init__(
self, *,
name: Optional[str] = None,
urls: Optional[Iterable[XsUri]] = None,
contacts: Optional[Iterable[OrganizationalContact]] = None,
address: Optional[PostalAddress] = None,
) -> None:
self.name = name
self.address = address
self.urls = urls or [] # type:ignore[assignment]
self.contacts = contacts or [] # type:ignore[assignment]
@property
@serializable.xml_sequence(10)
@serializable.xml_string(serializable.XmlStringSerializationType.NORMALIZED_STRING)
def name(self) -> Optional[str]:
"""
Get the name of the organization.
Returns:
`str` if set else `None`
"""
return self._name
@name.setter
def name(self, name: Optional[str]) -> None:
self._name = name
@property
@serializable.view(SchemaVersion1Dot6)
@serializable.xml_sequence(20)
def address(self) -> Optional[PostalAddress]:
"""
The physical address (location) of the organization.
Returns:
`PostalAddress` or `None`
"""
return self._address
@address.setter
def address(self, address: Optional[PostalAddress]) -> None:
self._address = address
@property
@serializable.json_name('url')
@serializable.xml_array(serializable.XmlArraySerializationType.FLAT, 'url')
@serializable.xml_sequence(30)
def urls(self) -> 'SortedSet[XsUri]':
"""
Get a list of URLs of the organization. Multiple URLs are allowed.
Returns:
Set of `XsUri`
"""
return self._urls
@urls.setter
def urls(self, urls: Iterable[XsUri]) -> None:
self._urls = SortedSet(urls)
@property
@serializable.json_name('contact')
@serializable.xml_array(serializable.XmlArraySerializationType.FLAT, 'contact')
@serializable.xml_sequence(40)
def contacts(self) -> 'SortedSet[OrganizationalContact]':
"""
Get a list of contact person at the organization. Multiple contacts are allowed.
Returns:
Set of `OrganizationalContact`
"""
return self._contacts
@contacts.setter
def contacts(self, contacts: Iterable[OrganizationalContact]) -> None:
self._contacts = SortedSet(contacts)
def __comparable_tuple(self) -> _ComparableTuple:
return _ComparableTuple((
self.name, _ComparableTuple(self.urls), _ComparableTuple(self.contacts)
))
def __eq__(self, other: object) -> bool:
if isinstance(other, OrganizationalEntity):
return self.__comparable_tuple() == other.__comparable_tuple()
return False
def __lt__(self, other: Any) -> bool:
if isinstance(other, OrganizationalEntity):
return self.__comparable_tuple() < other.__comparable_tuple()
return NotImplemented
def __hash__(self) -> int:
return hash(self.__comparable_tuple())
def __repr__(self) -> str:
return f'<OrganizationalEntity name={self.name}>'

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,623 @@
# 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
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# 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.
import re
from typing import TYPE_CHECKING, Any, Iterable, Optional, Union
import py_serializable as serializable
from sortedcontainers import SortedSet
from .._internal.bom_ref import bom_ref_from_str as _bom_ref_from_str
from .._internal.compare import ComparableTuple as _ComparableTuple
from ..exception.model import InvalidCreIdException
from ..exception.serialization import SerializationOfUnexpectedValueException
from . import ExternalReference, Property
from .bom_ref import BomRef
if TYPE_CHECKING: # pragma: no cover
from typing import Type, TypeVar
_T_CreId = TypeVar('_T_CreId', bound='CreId')
@serializable.serializable_class
class CreId(serializable.helpers.BaseHelper):
"""
Helper class that allows us to perform validation on data strings that must conform to
Common Requirements Enumeration (CRE) identifier(s).
"""
_VALID_CRE_REGEX = re.compile(r'^CRE:[0-9]+-[0-9]+$')
def __init__(self, id: str) -> None:
if CreId._VALID_CRE_REGEX.match(id) is None:
raise InvalidCreIdException(
f'Supplied value "{id} does not meet format specification.'
)
self._id = id
@property
@serializable.json_name('.')
@serializable.xml_name('.')
def id(self) -> str:
return self._id
@classmethod
def serialize(cls, o: Any) -> str:
if isinstance(o, cls):
return str(o)
raise SerializationOfUnexpectedValueException(
f'Attempt to serialize a non-CreId: {o!r}')
@classmethod
def deserialize(cls: 'Type[_T_CreId]', o: Any) -> '_T_CreId':
return cls(id=str(o))
def __eq__(self, other: Any) -> bool:
if isinstance(other, CreId):
return self._id == other._id
return False
def __lt__(self, other: Any) -> bool:
if isinstance(other, CreId):
return self._id < other._id
return NotImplemented
def __hash__(self) -> int:
return hash(self._id)
def __repr__(self) -> str:
return f'<CreId {self._id}>'
def __str__(self) -> str:
return self._id
@serializable.serializable_class
class Requirement:
"""
A requirement comprising a standard.
"""
def __init__(
self, *,
bom_ref: Optional[Union[str, BomRef]] = None,
identifier: Optional[str] = None,
title: Optional[str] = None,
text: Optional[str] = None,
descriptions: Optional[Iterable[str]] = None,
open_cre: Optional[Iterable[CreId]] = None,
parent: Optional[Union[str, BomRef]] = None,
properties: Optional[Iterable[Property]] = None,
external_references: Optional[Iterable[ExternalReference]] = None,
) -> None:
self._bom_ref = _bom_ref_from_str(bom_ref)
self.identifier = identifier
self.title = title
self.text = text
self.descriptions = descriptions or () # type:ignore[assignment]
self.open_cre = open_cre or () # type:ignore[assignment]
self.parent = parent # type:ignore[assignment]
self.properties = properties or () # type:ignore[assignment]
self.external_references = external_references or () # type:ignore[assignment]
@property
@serializable.type_mapping(BomRef)
@serializable.json_name('bom-ref')
@serializable.xml_name('bom-ref')
@serializable.xml_attribute()
def bom_ref(self) -> BomRef:
"""
An optional identifier which can be used to reference the requirement elsewhere in the BOM.
Every bom-ref MUST be unique within the BOM.
Returns:
`BomRef`
"""
return self._bom_ref
@property
@serializable.xml_sequence(1)
def identifier(self) -> Optional[str]:
"""
Returns:
The identifier of the requirement.
"""
return self._identifier
@identifier.setter
def identifier(self, identifier: Optional[str]) -> None:
self._identifier = identifier
@property
@serializable.xml_sequence(2)
def title(self) -> Optional[str]:
"""
Returns:
The title of the requirement.
"""
return self._title
@title.setter
def title(self, title: Optional[str]) -> None:
self._title = title
@property
@serializable.xml_sequence(3)
def text(self) -> Optional[str]:
"""
Returns:
The text of the requirement.
"""
return self._text
@text.setter
def text(self, text: Optional[str]) -> None:
self._text = text
@property
@serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'description')
@serializable.xml_sequence(4)
def descriptions(self) -> 'SortedSet[str]':
"""
Returns:
A SortedSet of descriptions of the requirement.
"""
return self._descriptions
@descriptions.setter
def descriptions(self, descriptions: Iterable[str]) -> None:
self._descriptions = SortedSet(descriptions)
@property
@serializable.json_name('openCre')
@serializable.xml_array(serializable.XmlArraySerializationType.FLAT, 'openCre')
@serializable.xml_sequence(5)
def open_cre(self) -> 'SortedSet[CreId]':
"""
CRE is a structured and standardized framework for uniting security standards and guidelines. CRE links each
section of a resource to a shared topic identifier (a Common Requirement). Through this shared topic link, all
resources map to each other. Use of CRE promotes clear and unambiguous communication among stakeholders.
Returns:
The Common Requirements Enumeration (CRE) identifier(s).
CREs must match regular expression: ^CRE:[0-9]+-[0-9]+$
"""
return self._open_cre
@open_cre.setter
def open_cre(self, open_cre: Iterable[CreId]) -> None:
self._open_cre = SortedSet(open_cre)
@property
@serializable.type_mapping(BomRef)
@serializable.xml_sequence(6)
def parent(self) -> Optional[BomRef]:
"""
Returns:
The optional bom-ref to a parent requirement. This establishes a hierarchy of requirements. Top-level
requirements must not define a parent. Only child requirements should define parents.
"""
return self._parent
@parent.setter
def parent(self, parent: Optional[Union[str, BomRef]]) -> None:
self._parent = _bom_ref_from_str(parent, optional=True)
@property
@serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'property')
@serializable.xml_sequence(7)
def properties(self) -> 'SortedSet[Property]':
"""
Provides the ability to document properties in a key/value store. This provides flexibility to include data not
officially supported in the standard without having to use additional namespaces or create extensions.
Return:
Set of `Property`
"""
return self._properties
@properties.setter
def properties(self, properties: Iterable[Property]) -> None:
self._properties = SortedSet(properties)
@property
@serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'reference')
@serializable.xml_sequence(8)
def external_references(self) -> 'SortedSet[ExternalReference]':
"""
Provides the ability to document external references related to the component or to the project the component
describes.
Returns:
Set of `ExternalReference`
"""
return self._external_references
@external_references.setter
def external_references(self, external_references: Iterable[ExternalReference]) -> None:
self._external_references = SortedSet(external_references)
def __comparable_tuple(self) -> _ComparableTuple:
# all properties are optional - so need to compare all, in hope that one is unique
return _ComparableTuple((
self.identifier, self.bom_ref.value,
self.title, self.text,
_ComparableTuple(self.descriptions),
_ComparableTuple(self.open_cre), self.parent, _ComparableTuple(self.properties),
_ComparableTuple(self.external_references)
))
def __lt__(self, other: Any) -> bool:
if isinstance(other, Requirement):
return self.__comparable_tuple() < other.__comparable_tuple()
return NotImplemented
def __eq__(self, other: object) -> bool:
if isinstance(other, Requirement):
return self.__comparable_tuple() == other.__comparable_tuple()
return False
def __hash__(self) -> int:
return hash(self.__comparable_tuple())
def __repr__(self) -> str:
return f'<Requirement bom-ref={self._bom_ref}, identifier={self.identifier}, ' \
f'title={self.title}, text={self.text}, parent={self.parent}>'
@serializable.serializable_class
class Level:
"""
Level of compliance for a standard.
"""
def __init__(
self, *,
bom_ref: Optional[Union[str, BomRef]] = None,
identifier: Optional[str] = None,
title: Optional[str] = None,
description: Optional[str] = None,
requirements: Optional[Iterable[Union[str, BomRef]]] = None,
) -> None:
self._bom_ref = _bom_ref_from_str(bom_ref)
self.identifier = identifier
self.title = title
self.description = description
self.requirements = requirements or () # type:ignore[assignment]
@property
@serializable.type_mapping(BomRef)
@serializable.json_name('bom-ref')
@serializable.xml_name('bom-ref')
@serializable.xml_attribute()
def bom_ref(self) -> BomRef:
"""
An optional identifier which can be used to reference the level elsewhere in the BOM.
Every bom-ref MUST be unique within the BOM.
Returns:
`BomRef`
"""
return self._bom_ref
@property
@serializable.xml_sequence(1)
def identifier(self) -> Optional[str]:
"""
Returns:
The identifier of the level.
"""
return self._identifier
@identifier.setter
def identifier(self, identifier: Optional[str]) -> None:
self._identifier = identifier
@property
@serializable.xml_sequence(2)
def title(self) -> Optional[str]:
"""
Returns:
The title of the level.
"""
return self._title
@title.setter
def title(self, title: Optional[str]) -> None:
self._title = title
@property
@serializable.xml_sequence(3)
def description(self) -> Optional[str]:
"""
Returns:
The description of the level.
"""
return self._description
@description.setter
def description(self, description: Optional[str]) -> None:
self._description = description
@property
@serializable.xml_sequence(4)
@serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'requirement')
def requirements(self) -> 'SortedSet[BomRef]':
"""
Returns:
A SortedSet of requirements associated with the level.
"""
return self._requirements
@requirements.setter
def requirements(self, requirements: Iterable[Union[str, BomRef]]) -> None:
self._requirements = SortedSet(map(_bom_ref_from_str, # type: ignore[arg-type]
requirements))
def __comparable_tuple(self) -> _ComparableTuple:
# all properties are optional - so need to compare all, in hope that one is unique
return _ComparableTuple((
self.identifier, self.bom_ref.value,
self.title, self.description,
_ComparableTuple(self.requirements)
))
def __lt__(self, other: Any) -> bool:
if isinstance(other, Level):
return self.__comparable_tuple() < other.__comparable_tuple()
return NotImplemented
def __eq__(self, other: object) -> bool:
if isinstance(other, Level):
return self.__comparable_tuple() == other.__comparable_tuple()
return False
def __hash__(self) -> int:
return hash(self.__comparable_tuple())
def __repr__(self) -> str:
return f'<Level bom-ref={self.bom_ref}, identifier={self.identifier}, ' \
f'title={self.title}, description={self.description}>'
@serializable.serializable_class
class Standard:
"""
A standard of regulations, industry or organizational-specific standards, maturity models, best practices,
or any other requirements.
"""
def __init__(
self, *,
bom_ref: Optional[Union[str, BomRef]] = None,
name: Optional[str] = None,
version: Optional[str] = None,
description: Optional[str] = None,
owner: Optional[str] = None,
requirements: Optional[Iterable[Requirement]] = None,
levels: Optional[Iterable[Level]] = None,
external_references: Optional[Iterable['ExternalReference']] = None
# TODO: signature
) -> None:
self._bom_ref = _bom_ref_from_str(bom_ref)
self.name = name
self.version = version
self.description = description
self.owner = owner
self.requirements = requirements or () # type:ignore[assignment]
self.levels = levels or () # type:ignore[assignment]
self.external_references = external_references or () # type:ignore[assignment]
# TODO: signature
@property
@serializable.type_mapping(BomRef)
@serializable.json_name('bom-ref')
@serializable.xml_name('bom-ref')
@serializable.xml_attribute()
def bom_ref(self) -> BomRef:
"""
An optional identifier which can be used to reference the standard elsewhere in the BOM. Every bom-ref MUST be
unique within the BOM.
Returns:
`BomRef`
"""
return self._bom_ref
@property
@serializable.xml_sequence(1)
def name(self) -> Optional[str]:
"""
Returns:
The name of the standard
"""
return self._name
@name.setter
def name(self, name: Optional[str]) -> None:
self._name = name
@property
@serializable.xml_sequence(2)
def version(self) -> Optional[str]:
"""
Returns:
The version of the standard
"""
return self._version
@version.setter
def version(self, version: Optional[str]) -> None:
self._version = version
@property
@serializable.xml_sequence(3)
def description(self) -> Optional[str]:
"""
Returns:
The description of the standard
"""
return self._description
@description.setter
def description(self, description: Optional[str]) -> None:
self._description = description
@property
@serializable.xml_sequence(4)
def owner(self) -> Optional[str]:
"""
Returns:
The owner of the standard, often the entity responsible for its release.
"""
return self._owner
@owner.setter
def owner(self, owner: Optional[str]) -> None:
self._owner = owner
@property
@serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'requirement')
@serializable.xml_sequence(5)
def requirements(self) -> 'SortedSet[Requirement]':
"""
Returns:
A SortedSet of requirements comprising the standard.
"""
return self._requirements
@requirements.setter
def requirements(self, requirements: Iterable[Requirement]) -> None:
self._requirements = SortedSet(requirements)
@property
@serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'level')
@serializable.xml_sequence(6)
def levels(self) -> 'SortedSet[Level]':
"""
Returns:
A SortedSet of levels associated with the standard. Some standards have different levels of compliance.
"""
return self._levels
@levels.setter
def levels(self, levels: Iterable[Level]) -> None:
self._levels = SortedSet(levels)
@property
@serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'reference')
@serializable.xml_sequence(7)
def external_references(self) -> 'SortedSet[ExternalReference]':
"""
Returns:
A SortedSet of external references associated with the standard.
"""
return self._external_references
@external_references.setter
def external_references(self, external_references: Iterable[ExternalReference]) -> None:
self._external_references = SortedSet(external_references)
# @property
# @serializable.xml_sequence(8)
# # MUST NOT RENDER FOR XML -- this is JSON only
# def signature(self) -> ...:
# ...
#
# @signature.setter
# def levels(self, signature: ...) -> None:
# ...
def __comparable_tuple(self) -> _ComparableTuple:
# all properties are optional - so need to apply all, in hope that one is unique
return _ComparableTuple((
self.name, self.version,
self.bom_ref.value,
self.description, self.owner,
_ComparableTuple(self.requirements), _ComparableTuple(self.levels),
_ComparableTuple(self.external_references)
))
def __lt__(self, other: Any) -> bool:
if isinstance(other, Standard):
return self.__comparable_tuple() < other.__comparable_tuple()
return NotImplemented
def __eq__(self, other: object) -> bool:
if isinstance(other, Standard):
return self.__comparable_tuple() == other.__comparable_tuple()
return False
def __hash__(self) -> int:
return hash(self.__comparable_tuple())
def __repr__(self) -> str:
return f'<Standard bom-ref={self.bom_ref}, ' \
f'name={self.name}, version={self.version}, ' \
f'description={self.description}, owner={self.owner}>'
@serializable.serializable_class(name='definitions')
class Definitions:
"""
The repository for definitions
"""
def __init__(
self, *,
standards: Optional[Iterable[Standard]] = None
) -> None:
self.standards = standards or () # type:ignore[assignment]
@property
@serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'standard')
@serializable.xml_sequence(1)
def standards(self) -> 'SortedSet[Standard]':
"""
Returns:
A SortedSet of Standards
"""
return self._standards
@standards.setter
def standards(self, standards: Iterable[Standard]) -> None:
self._standards = SortedSet(standards)
def __bool__(self) -> bool:
return len(self._standards) > 0
def __comparable_tuple(self) -> _ComparableTuple:
# all properties are optional - so need to apply all, in hope that one is unique
return _ComparableTuple(self._standards)
def __eq__(self, other: object) -> bool:
if isinstance(other, Definitions):
return self.__comparable_tuple() == other.__comparable_tuple()
return False
def __lt__(self, other: Any) -> bool:
if isinstance(other, Definitions):
return self.__comparable_tuple() < other.__comparable_tuple()
return NotImplemented
def __hash__(self) -> int:
return hash(self.__comparable_tuple())
def __repr__(self) -> str:
return f'<Definitions standards={self.standards!r} >'

View File

@@ -0,0 +1,116 @@
# 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
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# 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.
from abc import ABC, abstractmethod
from typing import Any, Iterable, List, Optional, Set
import py_serializable as serializable
from sortedcontainers import SortedSet
from .._internal.compare import ComparableTuple as _ComparableTuple
from ..exception.serialization import SerializationOfUnexpectedValueException
from .bom_ref import BomRef
class _DependencyRepositorySerializationHelper(serializable.helpers.BaseHelper):
""" THIS CLASS IS NON-PUBLIC API """
@classmethod
def serialize(cls, o: Any) -> List[str]:
if isinstance(o, (SortedSet, set)):
return [str(i.ref) for i in o]
raise SerializationOfUnexpectedValueException(
f'Attempt to serialize a non-DependencyRepository: {o!r}')
@classmethod
def deserialize(cls, o: Any) -> Set['Dependency']:
dependencies = set()
if isinstance(o, list):
for v in o:
dependencies.add(Dependency(ref=BomRef(value=v)))
return dependencies
@serializable.serializable_class
class Dependency:
"""
Models a Dependency within a BOM.
.. note::
See https://cyclonedx.org/docs/1.6/xml/#type_dependencyType
"""
def __init__(self, ref: BomRef, dependencies: Optional[Iterable['Dependency']] = None) -> None:
self.ref = ref
self.dependencies = dependencies or [] # type:ignore[assignment]
@property
@serializable.type_mapping(BomRef)
@serializable.xml_attribute()
def ref(self) -> BomRef:
return self._ref
@ref.setter
def ref(self, ref: BomRef) -> None:
self._ref = ref
@property
@serializable.json_name('dependsOn')
@serializable.type_mapping(_DependencyRepositorySerializationHelper)
@serializable.xml_array(serializable.XmlArraySerializationType.FLAT, 'dependency')
def dependencies(self) -> 'SortedSet[Dependency]':
return self._dependencies
@dependencies.setter
def dependencies(self, dependencies: Iterable['Dependency']) -> None:
self._dependencies = SortedSet(dependencies)
def dependencies_as_bom_refs(self) -> Set[BomRef]:
return set(map(lambda d: d.ref, self.dependencies))
def __comparable_tuple(self) -> _ComparableTuple:
return _ComparableTuple((
self.ref, _ComparableTuple(self.dependencies)
))
def __eq__(self, other: object) -> bool:
if isinstance(other, Dependency):
return self.__comparable_tuple() == other.__comparable_tuple()
return False
def __lt__(self, other: Any) -> bool:
if isinstance(other, Dependency):
return self.__comparable_tuple() < other.__comparable_tuple()
return NotImplemented
def __hash__(self) -> int:
return hash(self.__comparable_tuple())
def __repr__(self) -> str:
return f'<Dependency ref={self.ref!r}, targets={len(self.dependencies)}>'
class Dependable(ABC):
"""
Dependable objects can be part of the Dependency Graph
"""
@property
@abstractmethod
def bom_ref(self) -> BomRef:
... # pragma: no cover

View File

@@ -0,0 +1,106 @@
# 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
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# 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.
"""
This set of classes represents the data about Impact Analysis.
Impact Analysis is new for CycloneDX schema version 1.
.. note::
See the CycloneDX Schema extension definition https://cyclonedx.org/docs/1.6
"""
from enum import Enum
import py_serializable as serializable
@serializable.serializable_enum
class ImpactAnalysisAffectedStatus(str, Enum):
"""
Enum object that defines the permissible impact analysis affected states.
The vulnerability status of a given version or range of versions of a product.
The statuses 'affected' and 'unaffected' indicate that the version is affected or unaffected by the vulnerability.
The status 'unknown' indicates that it is unknown or unspecified whether the given version is affected. There can
be many reasons for an 'unknown' status, including that an investigation has not been undertaken or that a vendor
has not disclosed the status.
.. note::
See the CycloneDX Schema definition: https://cyclonedx.org/docs/1.6/#type_impactAnalysisAffectedStatusType
"""
AFFECTED = 'affected'
UNAFFECTED = 'unaffected'
UNKNOWN = 'unknown'
@serializable.serializable_enum
class ImpactAnalysisJustification(str, Enum):
"""
Enum object that defines the rationale of why the impact analysis state was asserted.
.. note::
See the CycloneDX Schema definition: https://cyclonedx.org/docs/1.6/#type_impactAnalysisJustificationType
"""
CODE_NOT_PRESENT = 'code_not_present'
CODE_NOT_REACHABLE = 'code_not_reachable'
PROTECTED_AT_PERIMITER = 'protected_at_perimeter'
PROTECTED_AT_RUNTIME = 'protected_at_runtime'
PROTECTED_BY_COMPILER = 'protected_by_compiler'
PROTECTED_BY_MITIGATING_CONTROL = 'protected_by_mitigating_control'
REQUIRES_CONFIGURATION = 'requires_configuration'
REQUIRES_DEPENDENCY = 'requires_dependency'
REQUIRES_ENVIRONMENT = 'requires_environment'
@serializable.serializable_enum
class ImpactAnalysisResponse(str, Enum):
"""
Enum object that defines the valid rationales as to why the impact analysis state was asserted.
.. note::
See the CycloneDX Schema definition: https://cyclonedx.org/docs/1.6/#type_impactAnalysisResponsesType
"""
CAN_NOT_FIX = 'can_not_fix'
ROLLBACK = 'rollback'
UPDATE = 'update'
WILL_NOT_FIX = 'will_not_fix'
WORKAROUND_AVAILABLE = 'workaround_available'
@serializable.serializable_enum
class ImpactAnalysisState(str, Enum):
"""
Enum object that defines the permissible impact analysis states.
.. note::
See the CycloneDX Schema definition: https://cyclonedx.org/docs/1.6/#type_impactAnalysisStateType
"""
RESOLVED = 'resolved'
RESOLVED_WITH_PEDIGREE = 'resolved_with_pedigree'
EXPLOITABLE = 'exploitable'
IN_TRIAGE = 'in_triage'
FALSE_POSITIVE = 'false_positive'
NOT_AFFECTED = 'not_affected'

View File

@@ -0,0 +1,250 @@
# 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
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# 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.
from enum import Enum
from typing import Any, Iterable, Optional
import py_serializable as serializable
from sortedcontainers import SortedSet
from .._internal.compare import ComparableTuple as _ComparableTuple
from . import XsUri
@serializable.serializable_enum
class IssueClassification(str, Enum):
"""
This is our internal representation of the enum `issueClassification`.
.. note::
See the CycloneDX Schema definition: https://cyclonedx.org/docs/1.6/xml/#type_issueClassification
"""
DEFECT = 'defect'
ENHANCEMENT = 'enhancement'
SECURITY = 'security'
@serializable.serializable_class
class IssueTypeSource:
"""
This is our internal representation ofa source within the IssueType complex type that can be used in multiple
places within a CycloneDX BOM document.
.. note::
See the CycloneDX Schema definition: https://cyclonedx.org/docs/1.6/xml/#type_issueType
"""
def __init__(
self, *,
name: Optional[str] = None,
url: Optional[XsUri] = None,
) -> None:
self.name = name
self.url = url
@property
@serializable.xml_string(serializable.XmlStringSerializationType.NORMALIZED_STRING)
def name(self) -> Optional[str]:
"""
The name of the source. For example "National Vulnerability Database", "NVD", and "Apache".
Returns:
`str` if set else `None`
"""
return self._name
@name.setter
def name(self, name: Optional[str]) -> None:
self._name = name
@property
def url(self) -> Optional[XsUri]:
"""
Optional url of the issue documentation as provided by the source.
Returns:
`XsUri` if set else `None`
"""
return self._url
@url.setter
def url(self, url: Optional[XsUri]) -> None:
self._url = url
def __comparable_tuple(self) -> _ComparableTuple:
return _ComparableTuple((
self.name, self.url
))
def __eq__(self, other: object) -> bool:
if isinstance(other, IssueTypeSource):
return self.__comparable_tuple() == other.__comparable_tuple()
return False
def __lt__(self, other: Any) -> bool:
if isinstance(other, IssueTypeSource):
return self.__comparable_tuple() < other.__comparable_tuple()
return NotImplemented
def __hash__(self) -> int:
return hash(self.__comparable_tuple())
def __repr__(self) -> str:
return f'<IssueTypeSource name={self._name}, url={self.url}>'
@serializable.serializable_class
class IssueType:
"""
This is our internal representation of an IssueType complex type that can be used in multiple places within
a CycloneDX BOM document.
.. note::
See the CycloneDX Schema definition: https://cyclonedx.org/docs/1.6/xml/#type_issueType
"""
def __init__(
self, *,
type: IssueClassification,
id: Optional[str] = None,
name: Optional[str] = None,
description: Optional[str] = None,
source: Optional[IssueTypeSource] = None,
references: Optional[Iterable[XsUri]] = None,
) -> None:
self.type = type
self.id = id
self.name = name
self.description = description
self.source = source
self.references = references or [] # type:ignore[assignment]
@property
@serializable.xml_attribute()
def type(self) -> IssueClassification:
"""
Specifies the type of issue.
Returns:
`IssueClassification`
"""
return self._type
@type.setter
def type(self, type: IssueClassification) -> None:
self._type = type
@property
@serializable.xml_sequence(1)
@serializable.xml_string(serializable.XmlStringSerializationType.NORMALIZED_STRING)
def id(self) -> Optional[str]:
"""
The identifier of the issue assigned by the source of the issue.
Returns:
`str` if set else `None`
"""
return self._id
@id.setter
def id(self, id: Optional[str]) -> None:
self._id = id
@property
@serializable.xml_sequence(2)
@serializable.xml_string(serializable.XmlStringSerializationType.NORMALIZED_STRING)
def name(self) -> Optional[str]:
"""
The name of the issue.
Returns:
`str` if set else `None`
"""
return self._name
@name.setter
def name(self, name: Optional[str]) -> None:
self._name = name
@property
@serializable.xml_sequence(3)
@serializable.xml_string(serializable.XmlStringSerializationType.NORMALIZED_STRING)
def description(self) -> Optional[str]:
"""
A description of the issue.
Returns:
`str` if set else `None`
"""
return self._description
@description.setter
def description(self, description: Optional[str]) -> None:
self._description = description
@property
@serializable.xml_sequence(4)
def source(self) -> Optional[IssueTypeSource]:
"""
The source of this issue.
Returns:
`IssueTypeSource` if set else `None`
"""
return self._source
@source.setter
def source(self, source: Optional[IssueTypeSource]) -> None:
self._source = source
@property
@serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'url')
@serializable.xml_sequence(5)
def references(self) -> 'SortedSet[XsUri]':
"""
Any reference URLs related to this issue.
Returns:
Set of `XsUri`
"""
return self._references
@references.setter
def references(self, references: Iterable[XsUri]) -> None:
self._references = SortedSet(references)
def __comparable_tuple(self) -> _ComparableTuple:
return _ComparableTuple((
self.type, self.id, self.name, self.description, self.source,
_ComparableTuple(self.references)
))
def __eq__(self, other: object) -> bool:
if isinstance(other, IssueType):
return self.__comparable_tuple() == other.__comparable_tuple()
return False
def __lt__(self, other: Any) -> bool:
if isinstance(other, IssueType):
return self.__comparable_tuple() < other.__comparable_tuple()
return NotImplemented
def __hash__(self) -> int:
return hash(self.__comparable_tuple())
def __repr__(self) -> str:
return f'<IssueType type={self.type}, id={self.id}, name={self.name}>'

View File

@@ -0,0 +1,463 @@
# 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
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# 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.
"""
License related things
"""
from enum import Enum
from json import loads as json_loads
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Type, Union
from warnings import warn
from xml.etree.ElementTree import Element # nosec B405
import py_serializable as serializable
from sortedcontainers import SortedSet
from .._internal.compare import ComparableTuple as _ComparableTuple
from ..exception.model import MutuallyExclusivePropertiesException
from ..exception.serialization import CycloneDxDeserializationException
from ..schema.schema import SchemaVersion1Dot6
from . import AttachedText, XsUri
@serializable.serializable_enum
class LicenseAcknowledgement(str, Enum):
"""
This is our internal representation of the `type_licenseAcknowledgementEnumerationType` ENUM type
within the CycloneDX standard.
.. note::
Introduced in CycloneDX v1.6
.. note::
See the CycloneDX Schema for hashType:
https://cyclonedx.org/docs/1.6/#type_licenseAcknowledgementEnumerationType
"""
CONCLUDED = 'concluded'
DECLARED = 'declared'
# In an error, the name of the enum was `LicenseExpressionAcknowledgement`.
# Even though this was changed, there might be some downstream usage of this symbol, so we keep it around ...
LicenseExpressionAcknowledgement = LicenseAcknowledgement
"""Deprecated alias for :class:`LicenseAcknowledgement`"""
@serializable.serializable_class(name='license')
class DisjunctiveLicense:
"""
This is our internal representation of `licenseType` complex type that can be used in multiple places within
a CycloneDX BOM document.
.. note::
See the CycloneDX Schema definition: https://cyclonedx.org/docs/1.6/json/#components_items_licenses
"""
def __init__(
self, *,
id: Optional[str] = None, name: Optional[str] = None,
text: Optional[AttachedText] = None, url: Optional[XsUri] = None,
acknowledgement: Optional[LicenseAcknowledgement] = None,
) -> None:
if not id and not name:
raise MutuallyExclusivePropertiesException('Either `id` or `name` MUST be supplied')
if id and name:
warn(
'Both `id` and `name` have been supplied - `name` will be ignored!',
category=RuntimeWarning, stacklevel=1
)
self._id = id
self._name = name if not id else None
self._text = text
self._url = url
self._acknowledgement = acknowledgement
@property
@serializable.xml_sequence(1)
def id(self) -> Optional[str]:
"""
A SPDX license ID.
.. note::
See the list of expected values:
https://cyclonedx.org/docs/1.6/json/#components_items_licenses_items_license_id
Returns:
`str` or `None`
"""
return self._id
@id.setter
def id(self, id: Optional[str]) -> None:
self._id = id
if id is not None:
self._name = None
@property
@serializable.xml_sequence(1)
@serializable.xml_string(serializable.XmlStringSerializationType.NORMALIZED_STRING)
def name(self) -> Optional[str]:
"""
If SPDX does not define the license used, this field may be used to provide the license name.
Returns:
`str` or `None`
"""
return self._name
@name.setter
def name(self, name: Optional[str]) -> None:
self._name = name
if name is not None:
self._id = None
@property
@serializable.xml_sequence(2)
def text(self) -> Optional[AttachedText]:
"""
Specifies the optional full text of the attachment
Returns:
`AttachedText` else `None`
"""
return self._text
@text.setter
def text(self, text: Optional[AttachedText]) -> None:
self._text = text
@property
@serializable.xml_sequence(3)
def url(self) -> Optional[XsUri]:
"""
The URL to the attachment file. If the attachment is a license or BOM, an externalReference should also be
specified for completeness.
Returns:
`XsUri` or `None`
"""
return self._url
@url.setter
def url(self, url: Optional[XsUri]) -> None:
self._url = url
# @property
# ...
# @serializable.view(SchemaVersion1Dot5)
# @serializable.view(SchemaVersion1Dot6)
# @serializable.xml_sequence(5)
# def licensing(self) -> ...:
# ... # TODO since CDX1.5
#
# @licensing.setter
# def licensing(self, ...) -> None:
# ... # TODO since CDX1.5
# @property
# ...
# @serializable.view(SchemaVersion1Dot5)
# @serializable.view(SchemaVersion1Dot6)
# @serializable.xml_sequence(6)
# def properties(self) -> ...:
# ... # TODO since CDX1.5
#
# @licensing.setter
# def properties(self, ...) -> None:
# ... # TODO since CDX1.5
# @property
# @serializable.json_name('bom-ref')
# @serializable.type_mapping(BomRefHelper)
# @serializable.view(SchemaVersion1Dot5)
# @serializable.view(SchemaVersion1Dot6)
# @serializable.xml_attribute()
# @serializable.xml_name('bom-ref')
# def bom_ref(self) -> BomRef:
# ... # TODO since CDX1.5
@property
@serializable.view(SchemaVersion1Dot6)
@serializable.xml_attribute()
def acknowledgement(self) -> Optional[LicenseAcknowledgement]:
"""
Declared licenses and concluded licenses represent two different stages in the licensing process within
software development.
Declared licenses refer to the initial intention of the software authors regarding the
licensing terms under which their code is released. On the other hand, concluded licenses are the result of a
comprehensive analysis of the project's codebase to identify and confirm the actual licenses of the components
used, which may differ from the initially declared licenses. While declared licenses provide an upfront
indication of the licensing intentions, concluded licenses offer a more thorough understanding of the actual
licensing within a project, facilitating proper compliance and risk management. Observed licenses are defined
in evidence.licenses. Observed licenses form the evidence necessary to substantiate a concluded license.
Returns:
`LicenseAcknowledgement` or `None`
"""
return self._acknowledgement
@acknowledgement.setter
def acknowledgement(self, acknowledgement: Optional[LicenseAcknowledgement]) -> None:
self._acknowledgement = acknowledgement
def __comparable_tuple(self) -> _ComparableTuple:
return _ComparableTuple((
self._acknowledgement,
self._id, self._name,
self._url,
self._text,
))
def __eq__(self, other: object) -> bool:
if isinstance(other, DisjunctiveLicense):
return self.__comparable_tuple() == other.__comparable_tuple()
return False
def __lt__(self, other: Any) -> bool:
if isinstance(other, DisjunctiveLicense):
return self.__comparable_tuple() < other.__comparable_tuple()
if isinstance(other, LicenseExpression):
return False # self after any LicenseExpression
return NotImplemented
def __hash__(self) -> int:
return hash(self.__comparable_tuple())
def __repr__(self) -> str:
return f'<License id={self._id!r}, name={self._name!r}>'
@serializable.serializable_class(name='expression')
class LicenseExpression:
"""
This is our internal representation of `licenseType`'s expression type that can be used in multiple places within
a CycloneDX BOM document.
.. note::
See the CycloneDX Schema definition:
https://cyclonedx.org/docs/1.6/json/#components_items_licenses_items_expression
"""
def __init__(
self, value: str, *,
acknowledgement: Optional[LicenseAcknowledgement] = None,
) -> None:
self._value = value
self._acknowledgement = acknowledgement
@property
@serializable.xml_name('.')
@serializable.xml_string(serializable.XmlStringSerializationType.NORMALIZED_STRING)
@serializable.json_name('expression')
def value(self) -> str:
"""
Value of this LicenseExpression.
Returns:
`str`
"""
return self._value
@value.setter
def value(self, value: str) -> None:
self._value = value
# @property
# @serializable.json_name('bom-ref')
# @serializable.type_mapping(BomRefHelper)
# @serializable.view(SchemaVersion1Dot5)
# @serializable.view(SchemaVersion1Dot6)
# @serializable.xml_attribute()
# @serializable.xml_name('bom-ref')
# def bom_ref(self) -> BomRef:
# ... # TODO since CDX1.5
@property
@serializable.view(SchemaVersion1Dot6)
@serializable.xml_attribute()
def acknowledgement(self) -> Optional[LicenseAcknowledgement]:
"""
Declared licenses and concluded licenses represent two different stages in the licensing process within
software development.
Declared licenses refer to the initial intention of the software authors regarding the
licensing terms under which their code is released. On the other hand, concluded licenses are the result of a
comprehensive analysis of the project's codebase to identify and confirm the actual licenses of the components
used, which may differ from the initially declared licenses. While declared licenses provide an upfront
indication of the licensing intentions, concluded licenses offer a more thorough understanding of the actual
licensing within a project, facilitating proper compliance and risk management. Observed licenses are defined
in evidence.licenses. Observed licenses form the evidence necessary to substantiate a concluded license.
Returns:
`LicenseAcknowledgement` or `None`
"""
return self._acknowledgement
@acknowledgement.setter
def acknowledgement(self, acknowledgement: Optional[LicenseAcknowledgement]) -> None:
self._acknowledgement = acknowledgement
def __comparable_tuple(self) -> _ComparableTuple:
return _ComparableTuple((
self._acknowledgement,
self._value,
))
def __hash__(self) -> int:
return hash(self.__comparable_tuple())
def __eq__(self, other: object) -> bool:
if isinstance(other, LicenseExpression):
return self.__comparable_tuple() == other.__comparable_tuple()
return False
def __lt__(self, other: Any) -> bool:
if isinstance(other, LicenseExpression):
return self.__comparable_tuple() < other.__comparable_tuple()
if isinstance(other, DisjunctiveLicense):
return True # self before any DisjunctiveLicense
return NotImplemented
def __repr__(self) -> str:
return f'<LicenseExpression value={self._value!r}>'
License = Union[LicenseExpression, DisjunctiveLicense]
"""TypeAlias for a union of supported license models.
- :class:`LicenseExpression`
- :class:`DisjunctiveLicense`
"""
if TYPE_CHECKING: # pragma: no cover
# workaround for https://github.com/python/mypy/issues/5264
# this code path is taken when static code analysis or documentation tools runs through.
class LicenseRepository(SortedSet[License]):
"""Collection of :class:`License`.
This is a `set`, not a `list`. Order MUST NOT matter here.
If you wanted a certain order, then you should also express whether the items are concat by `AND` or `OR`.
If you wanted to do so, you should use :class:`LicenseExpression`.
As a model, this MUST accept multiple :class:`LicenseExpression` along with
multiple :class:`DisjunctiveLicense`, as this was an accepted in CycloneDX JSON before v1.5.
So for modeling purposes, this is supported.
Denormalizers/deserializers will be thankful.
The normalization/serialization process SHOULD take care of these facts and do what is needed.
"""
else:
class LicenseRepository(SortedSet):
"""Collection of :class:`License`.
This is a `set`, not a `list`. Order MUST NOT matter here.
If you wanted a certain order, then you should also express whether the items are concat by `AND` or `OR`.
If you wanted to do so, you should use :class:`LicenseExpression`.
As a model, this MUST accept multiple :class:`LicenseExpression` along with
multiple :class:`DisjunctiveLicense`, as this was an accepted in CycloneDX JSON before v1.5.
So for modeling purposes, this is supported.
Denormalizers/deserializers will be thankful.
The normalization/serialization process SHOULD take care of these facts and do what is needed.
"""
class _LicenseRepositorySerializationHelper(serializable.helpers.BaseHelper):
""" THIS CLASS IS NON-PUBLIC API """
@classmethod
def json_normalize(cls, o: LicenseRepository, *,
view: Optional[Type[serializable.ViewType]],
**__: Any) -> Any:
if len(o) == 0:
return None
expression = next((li for li in o if isinstance(li, LicenseExpression)), None)
if expression:
# mixed license expression and license? this is an invalid constellation according to schema!
# see https://github.com/CycloneDX/specification/pull/205
# but models need to allow it for backwards compatibility with JSON CDX < 1.5
return [json_loads(expression.as_json(view_=view))] # type:ignore[attr-defined]
return [
{'license': json_loads(
li.as_json( # type:ignore[attr-defined]
view_=view)
)}
for li in o
if isinstance(li, DisjunctiveLicense)
]
@classmethod
def json_denormalize(cls, o: List[Dict[str, Any]],
**__: Any) -> LicenseRepository:
repo = LicenseRepository()
for li in o:
if 'license' in li:
repo.add(DisjunctiveLicense.from_json( # type:ignore[attr-defined]
li['license']))
elif 'expression' in li:
repo.add(LicenseExpression.from_json( # type:ignore[attr-defined]
li
))
else:
raise CycloneDxDeserializationException(f'unexpected: {li!r}')
return repo
@classmethod
def xml_normalize(cls, o: LicenseRepository, *,
element_name: str,
view: Optional[Type[serializable.ViewType]],
xmlns: Optional[str],
**__: Any) -> Optional[Element]:
if len(o) == 0:
return None
elem = Element(element_name)
expression = next((li for li in o if isinstance(li, LicenseExpression)), None)
if expression:
# mixed license expression and license? this is an invalid constellation according to schema!
# see https://github.com/CycloneDX/specification/pull/205
# but models need to allow it for backwards compatibility with JSON CDX < 1.5
elem.append(expression.as_xml( # type:ignore[attr-defined]
view_=view, as_string=False, element_name='expression', xmlns=xmlns))
else:
elem.extend(
li.as_xml( # type:ignore[attr-defined]
view_=view, as_string=False, element_name='license', xmlns=xmlns)
for li in o
if isinstance(li, DisjunctiveLicense)
)
return elem
@classmethod
def xml_denormalize(cls, o: Element,
default_ns: Optional[str],
**__: Any) -> LicenseRepository:
repo = LicenseRepository()
for li in o:
tag = li.tag if default_ns is None else li.tag.replace(f'{{{default_ns}}}', '')
if tag == 'license':
repo.add(DisjunctiveLicense.from_xml( # type:ignore[attr-defined]
li, default_ns))
elif tag == 'expression':
repo.add(LicenseExpression.from_xml( # type:ignore[attr-defined]
li, default_ns))
else:
raise CycloneDxDeserializationException(f'unexpected: {li!r}')
return repo

View File

@@ -0,0 +1,248 @@
# 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
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# 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.
"""
This set of classes represents the lifecycles types in the CycloneDX standard.
.. note::
Introduced in CycloneDX v1.5
.. note::
See the CycloneDX Schema for lifecycles: https://cyclonedx.org/docs/1.6/#metadata_lifecycles
"""
from enum import Enum
from json import loads as json_loads
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Type, Union
from xml.etree.ElementTree import Element # nosec B405
import py_serializable as serializable
from py_serializable.helpers import BaseHelper
from sortedcontainers import SortedSet
from .._internal.compare import ComparableTuple as _ComparableTuple
from ..exception.serialization import CycloneDxDeserializationException
if TYPE_CHECKING: # pragma: no cover
from py_serializable import ViewType
@serializable.serializable_enum
class LifecyclePhase(str, Enum):
"""
Enum object that defines the permissible 'phase' for a Lifecycle according to the CycloneDX schema.
.. note::
See the CycloneDX Schema definition: https://cyclonedx.org/docs/1.6/#type_classification
"""
DESIGN = 'design'
PRE_BUILD = 'pre-build'
BUILD = 'build'
POST_BUILD = 'post-build'
OPERATIONS = 'operations'
DISCOVERY = 'discovery'
DECOMMISSION = 'decommission'
@serializable.serializable_class
class PredefinedLifecycle:
"""
Object that defines pre-defined phases in the product lifecycle.
.. note::
See the CycloneDX Schema definition: https://cyclonedx.org/docs/1.6/#metadata_lifecycles
"""
def __init__(self, phase: LifecyclePhase) -> None:
self._phase = phase
@property
def phase(self) -> LifecyclePhase:
return self._phase
@phase.setter
def phase(self, phase: LifecyclePhase) -> None:
self._phase = phase
def __hash__(self) -> int:
return hash(self._phase)
def __eq__(self, other: object) -> bool:
if isinstance(other, PredefinedLifecycle):
return self._phase == other._phase
return False
def __lt__(self, other: Any) -> bool:
if isinstance(other, PredefinedLifecycle):
return self._phase < other._phase
if isinstance(other, NamedLifecycle):
return True # put PredefinedLifecycle before any NamedLifecycle
return NotImplemented
def __repr__(self) -> str:
return f'<PredefinedLifecycle phase={self._phase}>'
@serializable.serializable_class
class NamedLifecycle:
"""
Object that defines custom state in the product lifecycle.
.. note::
See the CycloneDX Schema definition: https://cyclonedx.org/docs/1.6/#metadata_lifecycles
"""
def __init__(self, name: str, *, description: Optional[str] = None) -> None:
self._name = name
self._description = description
@property
@serializable.xml_sequence(1)
@serializable.xml_string(serializable.XmlStringSerializationType.NORMALIZED_STRING)
def name(self) -> str:
"""
Name of the lifecycle phase.
Returns:
`str`
"""
return self._name
@name.setter
def name(self, name: str) -> None:
self._name = name
@property
@serializable.xml_sequence(2)
@serializable.xml_string(serializable.XmlStringSerializationType.NORMALIZED_STRING)
def description(self) -> Optional[str]:
"""
Description of the lifecycle phase.
Returns:
`str`
"""
return self._description
@description.setter
def description(self, description: Optional[str]) -> None:
self._description = description
def __comparable_tuple(self) -> _ComparableTuple:
return _ComparableTuple((
self._name, self._description
))
def __hash__(self) -> int:
return hash(self.__comparable_tuple())
def __eq__(self, other: object) -> bool:
if isinstance(other, NamedLifecycle):
return self.__comparable_tuple() == other.__comparable_tuple()
return False
def __lt__(self, other: Any) -> bool:
if isinstance(other, NamedLifecycle):
return self.__comparable_tuple() < other.__comparable_tuple()
if isinstance(other, PredefinedLifecycle):
return False # put NamedLifecycle after any PredefinedLifecycle
return NotImplemented
def __repr__(self) -> str:
return f'<NamedLifecycle name={self._name}>'
Lifecycle = Union[PredefinedLifecycle, NamedLifecycle]
"""TypeAlias for a union of supported lifecycle models.
- :class:`PredefinedLifecycle`
- :class:`NamedLifecycle`
"""
if TYPE_CHECKING: # pragma: no cover
# workaround for https://github.com/python/mypy/issues/5264
# this code path is taken when static code analysis or documentation tools runs through.
class LifecycleRepository(SortedSet[Lifecycle]):
"""Collection of :class:`Lifecycle`.
This is a `set`, not a `list`. Order MUST NOT matter here.
"""
else:
class LifecycleRepository(SortedSet):
"""Collection of :class:`Lifecycle`.
This is a `set`, not a `list`. Order MUST NOT matter here.
"""
class _LifecycleRepositoryHelper(BaseHelper):
@classmethod
def json_normalize(cls, o: LifecycleRepository, *,
view: Optional[Type['ViewType']],
**__: Any) -> Any:
if len(o) == 0:
return None
return [json_loads(li.as_json( # type:ignore[union-attr]
view_=view)) for li in o]
@classmethod
def json_denormalize(cls, o: List[Dict[str, Any]],
**__: Any) -> LifecycleRepository:
repo = LifecycleRepository()
for li in o:
if 'phase' in li:
repo.add(PredefinedLifecycle.from_json( # type:ignore[attr-defined]
li))
elif 'name' in li:
repo.add(NamedLifecycle.from_json( # type:ignore[attr-defined]
li))
else:
raise CycloneDxDeserializationException(f'unexpected: {li!r}')
return repo
@classmethod
def xml_normalize(cls, o: LifecycleRepository, *,
element_name: str,
view: Optional[Type['ViewType']],
xmlns: Optional[str],
**__: Any) -> Optional[Element]:
if len(o) == 0:
return None
elem = Element(element_name)
for li in o:
elem.append(li.as_xml( # type:ignore[union-attr]
view_=view, as_string=False, element_name='lifecycle', xmlns=xmlns))
return elem
@classmethod
def xml_denormalize(cls, o: Element,
default_ns: Optional[str],
**__: Any) -> LifecycleRepository:
repo = LifecycleRepository()
ns_map = {'bom': default_ns or ''}
# Do not iterate over `o` and do not check for expected `.tag` of items.
# This check could have been done by schema validators before even deserializing.
for li in o.iterfind('bom:lifecycle', ns_map):
if li.find('bom:phase', ns_map) is not None:
repo.add(PredefinedLifecycle.from_xml( # type:ignore[attr-defined]
li, default_ns))
elif li.find('bom:name', ns_map) is not None:
repo.add(NamedLifecycle.from_xml( # type:ignore[attr-defined]
li, default_ns))
else:
raise CycloneDxDeserializationException(f'unexpected content: {li!r}')
return repo

View File

@@ -0,0 +1,256 @@
# 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
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# 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.
from datetime import datetime
from typing import Iterable, Optional
import py_serializable as serializable
from sortedcontainers import SortedSet
from .._internal.compare import ComparableTuple as _ComparableTuple
from ..model import Note, Property, XsUri
from ..model.issue import IssueType
@serializable.serializable_class
class ReleaseNotes:
"""
This is our internal representation of a `releaseNotesType` for a Component in a BOM.
.. note::
See the CycloneDX Schema definition: https://cyclonedx.org/docs/1.6/#type_releaseNotesType
"""
def __init__(
self, *,
type: str, title: Optional[str] = None,
featured_image: Optional[XsUri] = None,
social_image: Optional[XsUri] = None,
description: Optional[str] = None,
timestamp: Optional[datetime] = None,
aliases: Optional[Iterable[str]] = None,
tags: Optional[Iterable[str]] = None,
resolves: Optional[Iterable[IssueType]] = None,
notes: Optional[Iterable[Note]] = None,
properties: Optional[Iterable[Property]] = None,
) -> None:
self.type = type
self.title = title
self.featured_image = featured_image
self.social_image = social_image
self.description = description
self.timestamp = timestamp
self.aliases = aliases or [] # type:ignore[assignment]
self.tags = tags or [] # type:ignore[assignment]
self.resolves = resolves or [] # type:ignore[assignment]
self.notes = notes or [] # type:ignore[assignment]
self.properties = properties or [] # type:ignore[assignment]
@property
@serializable.xml_sequence(1)
@serializable.xml_string(serializable.XmlStringSerializationType.NORMALIZED_STRING)
def type(self) -> str:
"""
The software versioning type.
It is **RECOMMENDED** that the release type use one of 'major', 'minor', 'patch', 'pre-release', or 'internal'.
Representing all possible software release types is not practical, so standardizing on the recommended values,
whenever possible, is strongly encouraged.
* **major** = A major release may contain significant changes or may introduce breaking changes.
* **minor** = A minor release, also known as an update, may contain a smaller number of changes than major
releases.
* **patch** = Patch releases are typically unplanned and may resolve defects or important security issues.
* **pre-release** = A pre-release may include alpha, beta, or release candidates and typically have limited
support. They provide the ability to preview a release prior to its general availability.
* **internal** = Internal releases are not for public consumption and are intended to be used exclusively by the
project or manufacturer that produced it.
"""
return self._type
@type.setter
def type(self, type: str) -> None:
self._type = type
@property
@serializable.xml_sequence(2)
def title(self) -> Optional[str]:
"""
The title of the release.
"""
return self._title
@title.setter
def title(self, title: Optional[str]) -> None:
self._title = title
@property
@serializable.xml_sequence(3)
def featured_image(self) -> Optional[XsUri]:
"""
The URL to an image that may be prominently displayed with the release note.
"""
return self._featured_image
@featured_image.setter
def featured_image(self, featured_image: Optional[XsUri]) -> None:
self._featured_image = featured_image
@property
@serializable.xml_sequence(4)
def social_image(self) -> Optional[XsUri]:
"""
The URL to an image that may be used in messaging on social media platforms.
"""
return self._social_image
@social_image.setter
def social_image(self, social_image: Optional[XsUri]) -> None:
self._social_image = social_image
@property
@serializable.xml_sequence(5)
def description(self) -> Optional[str]:
"""
A short description of the release.
"""
return self._description
@description.setter
def description(self, description: Optional[str]) -> None:
self._description = description
@property
@serializable.type_mapping(serializable.helpers.XsdDateTime)
@serializable.xml_sequence(6)
def timestamp(self) -> Optional[datetime]:
"""
The date and time (timestamp) when the release note was created.
"""
return self._timestamp
@timestamp.setter
def timestamp(self, timestamp: Optional[datetime]) -> None:
self._timestamp = timestamp
@property
@serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'alias')
@serializable.xml_string(serializable.XmlStringSerializationType.NORMALIZED_STRING)
@serializable.xml_sequence(7)
def aliases(self) -> 'SortedSet[str]':
"""
One or more alternate names the release may be referred to. This may include unofficial terms used by
development and marketing teams (e.g. code names).
Returns:
Set of `str`
"""
return self._aliases
@aliases.setter
def aliases(self, aliases: Iterable[str]) -> None:
self._aliases = SortedSet(aliases)
@property
@serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'tag')
@serializable.xml_string(serializable.XmlStringSerializationType.NORMALIZED_STRING)
@serializable.xml_sequence(8)
def tags(self) -> 'SortedSet[str]':
"""
One or more tags that may aid in search or retrieval of the release note.
Returns:
Set of `str`
"""
return self._tags
@tags.setter
def tags(self, tags: Iterable[str]) -> None:
self._tags = SortedSet(tags)
@property
@serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'issue')
@serializable.xml_sequence(9)
def resolves(self) -> 'SortedSet[IssueType]':
"""
A collection of issues that have been resolved.
Returns:
Set of `IssueType`
"""
return self._resolves
@resolves.setter
def resolves(self, resolves: Iterable[IssueType]) -> None:
self._resolves = SortedSet(resolves)
@property
@serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'note')
@serializable.xml_sequence(10)
def notes(self) -> 'SortedSet[Note]':
"""
Zero or more release notes containing the locale and content. Multiple note elements may be specified to support
release notes in a wide variety of languages.
Returns:
Set of `Note`
"""
return self._notes
@notes.setter
def notes(self, notes: Iterable[Note]) -> None:
self._notes = SortedSet(notes)
@property
@serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'property')
@serializable.xml_sequence(11)
def properties(self) -> 'SortedSet[Property]':
"""
Provides the ability to document properties in a name-value store. This provides flexibility to include data not
officially supported in the standard without having to use additional namespaces or create extensions. Unlike
key-value stores, properties support duplicate names, each potentially having different values.
Returns:
Set of `Property`
"""
return self._properties
@properties.setter
def properties(self, properties: Iterable[Property]) -> None:
self._properties = SortedSet(properties)
def __comparable_tuple(self) -> _ComparableTuple:
return _ComparableTuple((
self.type, self.title, self.featured_image, self.social_image, self.description, self.timestamp,
_ComparableTuple(self.aliases),
_ComparableTuple(self.tags),
_ComparableTuple(self.resolves),
_ComparableTuple(self.notes),
_ComparableTuple(self.properties)
))
def __eq__(self, other: object) -> bool:
if isinstance(other, ReleaseNotes):
return self.__comparable_tuple() == other.__comparable_tuple()
return False
def __hash__(self) -> int:
return hash(self.__comparable_tuple())
def __repr__(self) -> str:
return f'<ReleaseNotes type={self.type}, title={self.title}>'

View File

@@ -0,0 +1,380 @@
# 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
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# 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.
"""
This set of classes represents the data that is possible about known Services.
.. note::
See the CycloneDX Schema extension definition https://cyclonedx.org/docs/1.6/xml/#type_servicesType
"""
from typing import Any, Iterable, Optional, Union
import py_serializable as serializable
from sortedcontainers import SortedSet
from .._internal.bom_ref import bom_ref_from_str as _bom_ref_from_str
from .._internal.compare import ComparableTuple as _ComparableTuple
from ..schema.schema import SchemaVersion1Dot3, SchemaVersion1Dot4, SchemaVersion1Dot5, SchemaVersion1Dot6
from . import DataClassification, ExternalReference, Property, XsUri
from .bom_ref import BomRef
from .contact import OrganizationalEntity
from .dependency import Dependable
from .license import License, LicenseRepository, _LicenseRepositorySerializationHelper
from .release_note import ReleaseNotes
@serializable.serializable_class
class Service(Dependable):
"""
Class that models the `service` complex type in the CycloneDX schema.
.. note::
See the CycloneDX schema: https://cyclonedx.org/docs/1.6/xml/#type_service
"""
def __init__(
self, *,
name: str,
bom_ref: Optional[Union[str, BomRef]] = None,
provider: Optional[OrganizationalEntity] = None,
group: Optional[str] = None,
version: Optional[str] = None,
description: Optional[str] = None,
endpoints: Optional[Iterable[XsUri]] = None,
authenticated: Optional[bool] = None,
x_trust_boundary: Optional[bool] = None,
data: Optional[Iterable[DataClassification]] = None,
licenses: Optional[Iterable[License]] = None,
external_references: Optional[Iterable[ExternalReference]] = None,
properties: Optional[Iterable[Property]] = None,
services: Optional[Iterable['Service']] = None,
release_notes: Optional[ReleaseNotes] = None,
) -> None:
self._bom_ref = _bom_ref_from_str(bom_ref)
self.provider = provider
self.group = group
self.name = name
self.version = version
self.description = description
self.endpoints = endpoints or [] # type:ignore[assignment]
self.authenticated = authenticated
self.x_trust_boundary = x_trust_boundary
self.data = data or [] # type:ignore[assignment]
self.licenses = licenses or [] # type:ignore[assignment]
self.external_references = external_references or [] # type:ignore[assignment]
self.services = services or [] # type:ignore[assignment]
self.release_notes = release_notes
self.properties = properties or [] # type:ignore[assignment]
@property
@serializable.json_name('bom-ref')
@serializable.type_mapping(BomRef)
@serializable.xml_attribute()
@serializable.xml_name('bom-ref')
def bom_ref(self) -> BomRef:
"""
An optional identifier which can be used to reference the service elsewhere in the BOM. Uniqueness is enforced
within all elements and children of the root-level bom element.
Returns:
`BomRef` unique identifier for this Service
"""
return self._bom_ref
@property
@serializable.xml_sequence(1)
def provider(self) -> Optional[OrganizationalEntity]:
"""
Get the organization that provides the service.
Returns:
`OrganizationalEntity` if set else `None`
"""
return self._provider
@provider.setter
def provider(self, provider: Optional[OrganizationalEntity]) -> None:
self._provider = provider
@property
@serializable.xml_sequence(2)
@serializable.xml_string(serializable.XmlStringSerializationType.NORMALIZED_STRING)
def group(self) -> Optional[str]:
"""
The grouping name, namespace, or identifier. This will often be a shortened, single name of the company or
project that produced the service or domain name. Whitespace and special characters should be avoided.
Returns:
`str` if provided else `None`
"""
return self._group
@group.setter
def group(self, group: Optional[str]) -> None:
self._group = group
@property
@serializable.xml_sequence(3)
@serializable.xml_string(serializable.XmlStringSerializationType.NORMALIZED_STRING)
def name(self) -> str:
"""
The name of the service. This will often be a shortened, single name of the service.
Returns:
`str`
"""
return self._name
@name.setter
def name(self, name: str) -> None:
self._name = name
@property
@serializable.xml_sequence(4)
@serializable.xml_string(serializable.XmlStringSerializationType.NORMALIZED_STRING)
def version(self) -> Optional[str]:
"""
The service version.
Returns:
`str` if set else `None`
"""
return self._version
@version.setter
def version(self, version: Optional[str]) -> None:
self._version = version
@property
@serializable.xml_sequence(5)
@serializable.xml_string(serializable.XmlStringSerializationType.NORMALIZED_STRING)
def description(self) -> Optional[str]:
"""
Specifies a description for the service.
Returns:
`str` if set else `None`
"""
return self._description
@description.setter
def description(self, description: Optional[str]) -> None:
self._description = description
@property
@serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'endpoint')
@serializable.xml_sequence(6)
def endpoints(self) -> 'SortedSet[XsUri]':
"""
A list of endpoints URI's this service provides.
Returns:
Set of `XsUri`
"""
return self._endpoints
@endpoints.setter
def endpoints(self, endpoints: Iterable[XsUri]) -> None:
self._endpoints = SortedSet(endpoints)
@property
@serializable.xml_sequence(7)
def authenticated(self) -> Optional[bool]:
"""
A boolean value indicating if the service requires authentication. A value of true indicates the service
requires authentication prior to use.
A value of false indicates the service does not require authentication.
Returns:
`bool` if set else `None`
"""
return self._authenticated
@authenticated.setter
def authenticated(self, authenticated: Optional[bool]) -> None:
self._authenticated = authenticated
@property
@serializable.json_name('x-trust-boundary')
@serializable.xml_name('x-trust-boundary')
@serializable.xml_sequence(8)
def x_trust_boundary(self) -> Optional[bool]:
"""
A boolean value indicating if use of the service crosses a trust zone or boundary. A value of true indicates
that by using the service, a trust boundary is crossed.
A value of false indicates that by using the service, a trust boundary is not crossed.
Returns:
`bool` if set else `None`
"""
return self._x_trust_boundary
@x_trust_boundary.setter
def x_trust_boundary(self, x_trust_boundary: Optional[bool]) -> None:
self._x_trust_boundary = x_trust_boundary
# @property
# ...
# @serializable.view(SchemaVersion1Dot5)
# @serializable.xml_sequence(9)
# def trust_zone(self) -> ...:
# ... # since CDX1.5
#
# @trust_zone.setter
# def trust_zone(self, ...) -> None:
# ... # since CDX1.5
@property
@serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'classification')
@serializable.xml_sequence(10)
def data(self) -> 'SortedSet[DataClassification]':
"""
Specifies the data classification.
Returns:
Set of `DataClassification`
"""
# TODO since CDX1.5 also supports `dataflow`, not only `DataClassification`
return self._data
@data.setter
def data(self, data: Iterable[DataClassification]) -> None:
self._data = SortedSet(data)
@property
@serializable.type_mapping(_LicenseRepositorySerializationHelper)
@serializable.xml_sequence(11)
def licenses(self) -> LicenseRepository:
"""
A optional list of statements about how this Service is licensed.
Returns:
Set of `LicenseChoice`
"""
# TODO since CDX1.5 also supports `dataflow`, not only `DataClassification`
return self._licenses
@licenses.setter
def licenses(self, licenses: Iterable[License]) -> None:
self._licenses = LicenseRepository(licenses)
@property
@serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'reference')
@serializable.xml_sequence(12)
def external_references(self) -> 'SortedSet[ExternalReference]':
"""
Provides the ability to document external references related to the Service.
Returns:
Set of `ExternalReference`
"""
return self._external_references
@external_references.setter
def external_references(self, external_references: Iterable[ExternalReference]) -> None:
self._external_references = SortedSet(external_references)
@property
@serializable.view(SchemaVersion1Dot3)
@serializable.view(SchemaVersion1Dot4)
@serializable.view(SchemaVersion1Dot5)
@serializable.view(SchemaVersion1Dot6)
@serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'property')
@serializable.xml_sequence(13)
def properties(self) -> 'SortedSet[Property]':
"""
Provides the ability to document properties in a key/value store. This provides flexibility to include data not
officially supported in the standard without having to use additional namespaces or create extensions.
Return:
Set of `Property`
"""
return self._properties
@properties.setter
def properties(self, properties: Iterable[Property]) -> None:
self._properties = SortedSet(properties)
@property
@serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'service')
@serializable.xml_sequence(14)
def services(self) -> "SortedSet['Service']":
"""
A list of services included or deployed behind the parent service.
This is not a dependency tree.
It provides a way to specify a hierarchical representation of service assemblies.
Returns:
Set of `Service`
"""
return self._services
@services.setter
def services(self, services: Iterable['Service']) -> None:
self._services = SortedSet(services)
@property
@serializable.view(SchemaVersion1Dot4)
@serializable.view(SchemaVersion1Dot5)
@serializable.view(SchemaVersion1Dot6)
@serializable.xml_sequence(15)
def release_notes(self) -> Optional[ReleaseNotes]:
"""
Specifies optional release notes.
Returns:
`ReleaseNotes` or `None`
"""
return self._release_notes
@release_notes.setter
def release_notes(self, release_notes: Optional[ReleaseNotes]) -> None:
self._release_notes = release_notes
def __comparable_tuple(self) -> _ComparableTuple:
return _ComparableTuple((
self.group, self.name, self.version,
self.bom_ref.value,
self.provider, self.description,
self.authenticated, _ComparableTuple(self.data), _ComparableTuple(self.endpoints),
_ComparableTuple(self.external_references), _ComparableTuple(self.licenses),
_ComparableTuple(self.properties), self.release_notes, _ComparableTuple(self.services),
self.x_trust_boundary
))
def __eq__(self, other: object) -> bool:
if isinstance(other, Service):
return self.__comparable_tuple() == other.__comparable_tuple()
return False
def __lt__(self, other: Any) -> bool:
if isinstance(other, Service):
return self.__comparable_tuple() < other.__comparable_tuple()
return NotImplemented
def __hash__(self) -> int:
return hash(self.__comparable_tuple())
def __repr__(self) -> str:
return f'<Service bom-ref={self.bom_ref}, group={self.group}, name={self.name}, version={self.version}>'

View File

@@ -0,0 +1,373 @@
# 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
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# 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.
from itertools import chain
from typing import TYPE_CHECKING, Any, Dict, Iterable, List, Optional, Type, Union
from warnings import warn
from xml.etree.ElementTree import Element # nosec B405
import py_serializable as serializable
from py_serializable.helpers import BaseHelper
from sortedcontainers import SortedSet
from .._internal.compare import ComparableTuple as _ComparableTuple
from ..schema import SchemaVersion
from ..schema.schema import SchemaVersion1Dot4, SchemaVersion1Dot5, SchemaVersion1Dot6
from . import ExternalReference, HashType, _HashTypeRepositorySerializationHelper
from .component import Component
from .service import Service
if TYPE_CHECKING: # pragma: no cover
from py_serializable import ObjectMetadataLibrary, ViewType
@serializable.serializable_class
class Tool:
"""
This is our internal representation of the `toolType` complex type within the CycloneDX standard.
Tool(s) are the things used in the creation of the CycloneDX document.
Tool might be deprecated since CycloneDX 1.5, but it is not deprecated in this library.
In fact, this library will try to provide a compatibility layer if needed.
.. note::
See the CycloneDX Schema for toolType: https://cyclonedx.org/docs/1.6/#type_toolType
"""
def __init__(
self, *,
vendor: Optional[str] = None,
name: Optional[str] = None,
version: Optional[str] = None,
hashes: Optional[Iterable[HashType]] = None,
external_references: Optional[Iterable[ExternalReference]] = None,
) -> None:
self.vendor = vendor
self.name = name
self.version = version
self.hashes = hashes or () # type:ignore[assignment]
self.external_references = external_references or () # type:ignore[assignment]
@property
@serializable.xml_sequence(1)
@serializable.xml_string(serializable.XmlStringSerializationType.NORMALIZED_STRING)
def vendor(self) -> Optional[str]:
"""
The name of the vendor who created the tool.
Returns:
`str` if set else `None`
"""
return self._vendor
@vendor.setter
def vendor(self, vendor: Optional[str]) -> None:
self._vendor = vendor
@property
@serializable.xml_sequence(2)
@serializable.xml_string(serializable.XmlStringSerializationType.NORMALIZED_STRING)
def name(self) -> Optional[str]:
"""
The name of the tool.
Returns:
`str` if set else `None`
"""
return self._name
@name.setter
def name(self, name: Optional[str]) -> None:
self._name = name
@property
@serializable.xml_sequence(3)
@serializable.xml_string(serializable.XmlStringSerializationType.NORMALIZED_STRING)
def version(self) -> Optional[str]:
"""
The version of the tool.
Returns:
`str` if set else `None`
"""
return self._version
@version.setter
def version(self, version: Optional[str]) -> None:
self._version = version
@property
@serializable.type_mapping(_HashTypeRepositorySerializationHelper)
@serializable.xml_sequence(4)
def hashes(self) -> 'SortedSet[HashType]':
"""
The hashes of the tool (if applicable).
Returns:
Set of `HashType`
"""
return self._hashes
@hashes.setter
def hashes(self, hashes: Iterable[HashType]) -> None:
self._hashes = SortedSet(hashes)
@property
@serializable.view(SchemaVersion1Dot4)
@serializable.view(SchemaVersion1Dot5)
@serializable.view(SchemaVersion1Dot6)
@serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'reference')
@serializable.xml_sequence(5)
def external_references(self) -> 'SortedSet[ExternalReference]':
"""
External References provides a way to document systems, sites, and information that may be relevant but which
are not included with the BOM.
Returns:
Set of `ExternalReference`
"""
return self._external_references
@external_references.setter
def external_references(self, external_references: Iterable[ExternalReference]) -> None:
self._external_references = SortedSet(external_references)
def __comparable_tuple(self) -> _ComparableTuple:
return _ComparableTuple((
self.vendor, self.name, self.version,
_ComparableTuple(self.hashes), _ComparableTuple(self.external_references)
))
def __eq__(self, other: object) -> bool:
if isinstance(other, Tool):
return self.__comparable_tuple() == other.__comparable_tuple()
return False
def __lt__(self, other: Any) -> bool:
if isinstance(other, Tool):
return self.__comparable_tuple() < other.__comparable_tuple()
return NotImplemented
def __hash__(self) -> int:
return hash(self.__comparable_tuple())
def __repr__(self) -> str:
return f'<Tool name={self.name}, version={self.version}, vendor={self.vendor}>'
@classmethod
def from_component(cls: Type['Tool'], component: 'Component') -> 'Tool':
return cls(
vendor=component.group,
name=component.name,
version=component.version,
hashes=component.hashes,
external_references=component.external_references,
)
@classmethod
def from_service(cls: Type['Tool'], service: 'Service') -> 'Tool':
return cls(
vendor=service.group,
name=service.name,
version=service.version,
external_references=service.external_references,
)
class ToolRepository:
"""
The repository of tool formats
"""
def __init__(
self, *,
components: Optional[Iterable[Component]] = None,
services: Optional[Iterable[Service]] = None,
# Deprecated since v1.5
tools: Optional[Iterable[Tool]] = None
) -> None:
if tools:
warn('`@.tools` is deprecated from CycloneDX v1.5 onwards. '
'Please use `@.components` and `@.services` instead.',
DeprecationWarning)
self.components = components or () # type:ignore[assignment]
self.services = services or () # type:ignore[assignment]
self.tools = tools or () # type:ignore[assignment]
@property
def components(self) -> 'SortedSet[Component]':
"""
Returns:
A SortedSet of Components
"""
return self._components
@components.setter
def components(self, components: Iterable[Component]) -> None:
self._components = SortedSet(components)
@property
def services(self) -> 'SortedSet[Service]':
"""
Returns:
A SortedSet of Services
"""
return self._services
@services.setter
def services(self, services: Iterable[Service]) -> None:
self._services = SortedSet(services)
@property
def tools(self) -> 'SortedSet[Tool]':
return self._tools
@tools.setter
def tools(self, tools: Iterable[Tool]) -> None:
self._tools = SortedSet(tools)
def __len__(self) -> int:
return len(self._tools) \
+ len(self._components) \
+ len(self._services)
def __bool__(self) -> bool:
return len(self._tools) > 0 \
or len(self._components) > 0 \
or len(self._services) > 0
def __comparable_tuple(self) -> _ComparableTuple:
return _ComparableTuple((
_ComparableTuple(self._tools),
_ComparableTuple(self._components),
_ComparableTuple(self._services)
))
def __eq__(self, other: object) -> bool:
if isinstance(other, ToolRepository):
return self.__comparable_tuple() == other.__comparable_tuple()
return False
def __hash__(self) -> int:
return hash(self.__comparable_tuple())
class _ToolRepositoryHelper(BaseHelper):
@staticmethod
def __all_as_tools(o: ToolRepository) -> 'SortedSet[Tool]':
# use a set here, so the collection gets deduplicated.
# use SortedSet set here, so the order stays reproducible.
return SortedSet(chain(
o.tools,
map(Tool.from_component, o.components),
map(Tool.from_service, o.services),
))
@staticmethod
def __supports_components_and_services(view: Any) -> bool:
try:
return view is not None and view().schema_version_enum >= SchemaVersion.V1_5
except Exception: # pragma: no cover
return False
@classmethod
def json_normalize(cls, o: ToolRepository, *,
view: Optional[Type['ViewType']],
**__: Any) -> Any:
if len(o.tools) > 0 or not cls.__supports_components_and_services(view):
ts = cls.__all_as_tools(o)
return tuple(ts) if ts else None
elem: Dict[str, Any] = {}
if o.components:
elem['components'] = tuple(o.components)
if o.services:
elem['services'] = tuple(o.services)
return elem or None
@classmethod
def json_denormalize(cls, o: Union[List[Dict[str, Any]], Dict[str, Any]],
**__: Any) -> ToolRepository:
tools = None
components = None
services = None
if isinstance(o, Dict):
components = map(lambda c: Component.from_json( # type:ignore[attr-defined]
c), o.get('components', ()))
services = map(lambda s: Service.from_json( # type:ignore[attr-defined]
s), o.get('services', ()))
elif isinstance(o, Iterable):
tools = map(lambda t: Tool.from_json( # type:ignore[attr-defined]
t), o)
return ToolRepository(components=components, services=services, tools=tools)
@classmethod
def xml_normalize(cls, o: ToolRepository, *,
element_name: str,
view: Optional[Type['ViewType']],
xmlns: Optional[str],
**__: Any) -> Optional[Element]:
elem = Element(element_name)
if len(o.tools) > 0 or not cls.__supports_components_and_services(view):
elem.extend(
ti.as_xml( # type:ignore[attr-defined]
view_=view, as_string=False, element_name='tool', xmlns=xmlns)
for ti in cls.__all_as_tools(o)
)
else:
if o.components:
elem_c = Element(f'{{{xmlns}}}components' if xmlns else 'components')
elem_c.extend(
ci.as_xml( # type:ignore[attr-defined]
view_=view, as_string=False, element_name='component', xmlns=xmlns)
for ci in o.components)
elem.append(elem_c)
if o.services:
elem_s = Element(f'{{{xmlns}}}services' if xmlns else 'services')
elem_s.extend(
si.as_xml( # type:ignore[attr-defined]
view_=view, as_string=False, element_name='service', xmlns=xmlns)
for si in o.services)
elem.append(elem_s)
return elem \
if len(elem) > 0 \
else None
@classmethod
def xml_denormalize(cls, o: Element, *,
default_ns: Optional[str],
prop_info: 'ObjectMetadataLibrary.SerializableProperty',
ctx: Type[Any],
**kwargs: Any) -> ToolRepository:
ns_map = {'bom': default_ns or ''}
# Do not iterate over `o` and do not check for expected `.tag` of items.
# This check could have been done by schema validators before even deserializing.
tools = None
components = None
services = None
ts = o.findall('bom:tool', ns_map)
if len(ts) > 0:
tools = map(lambda t: Tool.from_xml( # type:ignore[attr-defined]
t, default_ns), ts)
else:
components = map(lambda c: Component.from_xml( # type:ignore[attr-defined]
c, default_ns), o.iterfind('./bom:components/bom:component', ns_map))
services = map(lambda s: Service.from_xml( # type:ignore[attr-defined]
s, default_ns), o.iterfind('./bom:services/bom:service', ns_map))
return ToolRepository(components=components, services=services, tools=tools)

File diff suppressed because it is too large Load Diff