updates
This commit is contained in:
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
748
Backend/venv/lib/python3.12/site-packages/cyclonedx/model/bom.py
Normal file
748
Backend/venv/lib/python3.12/site-packages/cyclonedx/model/bom.py
Normal 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)}>'
|
||||
@@ -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
@@ -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}>'
|
||||
1598
Backend/venv/lib/python3.12/site-packages/cyclonedx/model/crypto.py
Normal file
1598
Backend/venv/lib/python3.12/site-packages/cyclonedx/model/crypto.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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} >'
|
||||
@@ -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
|
||||
@@ -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'
|
||||
@@ -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}>'
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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}>'
|
||||
@@ -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}>'
|
||||
@@ -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
Reference in New Issue
Block a user