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

View File

@@ -0,0 +1,25 @@
# 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.
"""
Python library for CycloneDX
"""
# !! version is managed by semantic_release
# do not use typing here, or else `semantic_release` might have issues finding the variable
__version__ = "9.1.0" # noqa:Q000

View File

@@ -0,0 +1,25 @@
# 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.
"""
!!! ALL SYMBOLS IN HERE ARE INTERNAL.
Everything might change without any notice.
"""
# THIS FILE IS INTENDED TO BE EMPTY.
# Put symbols in own modules/packages, not in this file!

View File

@@ -0,0 +1,51 @@
# 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.
"""
!!! ALL SYMBOLS IN HERE ARE INTERNAL.
Everything might change without any notice.
"""
from typing import Literal, Optional, Union, overload
from ..model.bom_ref import BomRef
@overload
def bom_ref_from_str(bom_ref: BomRef, optional: bool = ...) -> BomRef:
... # pragma: no cover
@overload
def bom_ref_from_str(bom_ref: Optional[str], optional: Literal[False] = False) -> BomRef:
... # pragma: no cover
@overload
def bom_ref_from_str(bom_ref: Optional[str], optional: Literal[True] = ...) -> Optional[BomRef]:
... # pragma: no cover
def bom_ref_from_str(bom_ref: Optional[Union[str, BomRef]], optional: bool = False) -> Optional[BomRef]:
if isinstance(bom_ref, BomRef):
return bom_ref
if bom_ref:
return BomRef(value=str(bom_ref))
return None \
if optional \
else BomRef()

View File

@@ -0,0 +1,82 @@
# 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.
"""
!!! ALL SYMBOLS IN HERE ARE INTERNAL.
Everything might change without any notice.
"""
from itertools import zip_longest
from typing import TYPE_CHECKING, Any, Dict, Optional, Tuple
if TYPE_CHECKING: # pragma: no cover
from packageurl import PackageURL
class ComparableTuple(Tuple[Optional[Any], ...]):
"""
Allows comparison of tuples, allowing for None values.
"""
def __lt__(self, other: Any) -> bool:
for s, o in zip_longest(self, other):
if s == o:
continue
# the idea is to have any consistent order, not necessarily "natural" order.
if s is None:
return False
if o is None:
return True
return bool(s < o)
return False
def __gt__(self, other: Any) -> bool:
for s, o in zip_longest(self, other):
if s == o:
continue
# the idea is to have any consistent order, not necessarily "natural" order.
if s is None:
return True
if o is None:
return False
return bool(s > o)
return False
class ComparableDict(ComparableTuple):
"""
Allows comparison of dictionaries, allowing for missing/None values.
"""
def __new__(cls, d: Dict[Any, Any]) -> 'ComparableDict':
return super(ComparableDict, cls).__new__(cls, sorted(d.items()))
class ComparablePackageURL(ComparableTuple):
"""
Allows comparison of PackageURL, allowing for qualifiers.
"""
def __new__(cls, p: 'PackageURL') -> 'ComparablePackageURL':
return super(ComparablePackageURL, cls).__new__(cls, (
p.type,
p.namespace,
p.version,
ComparableDict(p.qualifiers) if isinstance(p.qualifiers, dict) else p.qualifiers,
p.subpath
))

View File

@@ -0,0 +1,43 @@
# 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.
"""
!!! ALL SYMBOLS IN HERE ARE INTERNAL.
Everything might change without any notice.
"""
from hashlib import sha1
def file_sha1sum(filename: str) -> str:
"""
Generate a SHA1 hash of the provided file.
Args:
filename:
Absolute path to file to hash as `str`
Returns:
SHA-1 hash
"""
h = sha1() # nosec B303, B324
with open(filename, 'rb') as f:
for byte_block in iter(lambda: f.read(4096), b''):
h.update(byte_block)
return h.hexdigest()

View File

@@ -0,0 +1,29 @@
# 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.
"""
!!! ALL SYMBOLS IN HERE ARE INTERNAL.
Everything might change without any notice.
"""
from datetime import datetime, timezone
def get_now_utc() -> datetime:
return datetime.now(tz=timezone.utc)

View File

@@ -0,0 +1,20 @@
# 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.
"""
Builders used in this library.
"""

View File

@@ -0,0 +1,83 @@
# 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.
"""Representation of this very python library."""
__all__ = ['this_component', 'this_tool', ]
from .. import __version__ as __ThisVersion # noqa: N812
from ..model import ExternalReference, ExternalReferenceType, XsUri
from ..model.component import Component, ComponentType
from ..model.license import DisjunctiveLicense, LicenseAcknowledgement
from ..model.tool import Tool
# !!! keep this file in sync with `pyproject.toml`
def this_component() -> Component:
"""Representation of this very python library as a :class:`Component`."""
return Component(
type=ComponentType.LIBRARY,
group='CycloneDX',
name='cyclonedx-python-lib',
version=__ThisVersion or 'UNKNOWN',
description='Python library for CycloneDX',
licenses=(DisjunctiveLicense(id='Apache-2.0',
acknowledgement=LicenseAcknowledgement.DECLARED),),
external_references=(
# let's assume this is not a fork
ExternalReference(
type=ExternalReferenceType.WEBSITE,
url=XsUri('https://github.com/CycloneDX/cyclonedx-python-lib/#readme')
),
ExternalReference(
type=ExternalReferenceType.DOCUMENTATION,
url=XsUri('https://cyclonedx-python-library.readthedocs.io/')
),
ExternalReference(
type=ExternalReferenceType.VCS,
url=XsUri('https://github.com/CycloneDX/cyclonedx-python-lib')
),
ExternalReference(
type=ExternalReferenceType.BUILD_SYSTEM,
url=XsUri('https://github.com/CycloneDX/cyclonedx-python-lib/actions')
),
ExternalReference(
type=ExternalReferenceType.ISSUE_TRACKER,
url=XsUri('https://github.com/CycloneDX/cyclonedx-python-lib/issues')
),
ExternalReference(
type=ExternalReferenceType.LICENSE,
url=XsUri('https://github.com/CycloneDX/cyclonedx-python-lib/blob/main/LICENSE')
),
ExternalReference(
type=ExternalReferenceType.RELEASE_NOTES,
url=XsUri('https://github.com/CycloneDX/cyclonedx-python-lib/blob/main/CHANGELOG.md')
),
# we cannot assert where the lib was fetched from, but we can give a hint
ExternalReference(
type=ExternalReferenceType.DISTRIBUTION,
url=XsUri('https://pypi.org/project/cyclonedx-python-lib/')
),
),
# to be extended...
)
def this_tool() -> Tool:
"""Representation of this very python library as a :class:`Tool`."""
return Tool.from_component(this_component())

View File

@@ -0,0 +1,33 @@
# 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.
"""
Exceptions that are specific to the CycloneDX library implementation.
"""
class CycloneDxException(Exception): # noqa: N818
"""
Root exception thrown by this library.
"""
pass
class MissingOptionalDependencyException(CycloneDxException): # noqa: N818
"""Validation did not happen, due to missing dependencies."""
pass

View File

@@ -0,0 +1,58 @@
# 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.
"""
Exceptions relating to specific conditions that occur when factoring a model.
"""
from . import CycloneDxException
class CycloneDxFactoryException(CycloneDxException):
"""
Base exception that covers all exceptions that may be thrown during model factoring..
"""
pass
class LicenseChoiceFactoryException(CycloneDxFactoryException):
"""
Base exception that covers all LicenseChoiceFactory exceptions.
"""
pass
class InvalidSpdxLicenseException(LicenseChoiceFactoryException):
"""
Thrown when an invalid SPDX License is provided.
"""
pass
class LicenseFactoryException(CycloneDxFactoryException):
"""
Base exception that covers all LicenseFactory exceptions.
"""
pass
class InvalidLicenseExpressionException(LicenseFactoryException):
"""
Thrown when an invalid License expressions is provided.
"""
pass

View File

@@ -0,0 +1,133 @@
# 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.
"""
Exceptions relating to specific conditions that occur when modelling CycloneDX BOM.
"""
from . import CycloneDxException
class CycloneDxModelException(CycloneDxException):
"""
Base exception that covers all exceptions that may be thrown during model creation.
"""
pass
class InvalidLocaleTypeException(CycloneDxModelException):
"""
Raised when the supplied locale does not conform to ISO-639 specification.
Good examples:
- en
- en-US
- en-GB
- fr
- fr-CA
The language code MUST be lowercase. If the country code is specified, the country code MUST be upper case.
The language code and country code MUST be separated by a minus sign.
"""
pass
class InvalidNistQuantumSecurityLevelException(CycloneDxModelException):
"""
Raised when an invalid value is provided for an NIST Quantum Security Level
as defined at https://csrc.nist.gov/projects/post-quantum-cryptography/post-quantum-cryptography-standardization/
evaluation-criteria/security-(evaluation-criteria).
"""
pass
class InvalidOmniBorIdException(CycloneDxModelException):
"""
Raised when a supplied value for an OmniBOR ID does not meet the format requirements
as defined at https://www.iana.org/assignments/uri-schemes/prov/gitoid.
"""
pass
class InvalidRelatedCryptoMaterialSizeException(CycloneDxModelException):
"""
Raised when the supplied size of a Related Crypto Material is negative.
"""
pass
class InvalidSwhidException(CycloneDxModelException):
"""
Raised when a supplied value for an Swhid does not meet the format requirements
as defined at https://docs.softwareheritage.org/devel/swh-model/persistent-identifiers.html.
"""
pass
class InvalidUriException(CycloneDxModelException):
"""
Raised when a `str` is provided that needs to be a valid URI, but isn't.
"""
pass
class MutuallyExclusivePropertiesException(CycloneDxModelException):
"""
Raised when mutually exclusive properties are provided.
"""
pass
class NoPropertiesProvidedException(CycloneDxModelException):
"""
Raised when attempting to construct a model class and providing NO values (where all properites are defined as
Optional, but at least one is required).
"""
pass
class UnknownComponentDependencyException(CycloneDxModelException):
"""
Exception raised when a dependency has been noted for a Component that is NOT a Component BomRef in this Bom.
"""
pass
class UnknownHashTypeException(CycloneDxModelException):
"""
Exception raised when we are unable to determine the type of hash from a composite hash string.
"""
pass
class LicenseExpressionAlongWithOthersException(CycloneDxModelException):
"""
Exception raised when a LicenseExpression was detected along with other licenses.
If a LicenseExpression exists, than it must stand alone.
See https://github.com/CycloneDX/specification/pull/205
"""
pass
class InvalidCreIdException(CycloneDxModelException):
"""
Raised when a supplied value for an CRE ID does not meet the format requirements
as defined at https://opencre.org/
"""
pass

View File

@@ -0,0 +1,39 @@
# 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.
"""
Exceptions that are for specific error scenarios during the output of a Model to a SBOM.
"""
from . import CycloneDxException
class BomGenerationErrorException(CycloneDxException):
"""
Raised if there is an unknown error.
"""
pass
class FormatNotSupportedException(CycloneDxException):
"""
Exception raised when attempting to output a BOM to a format not supported in the requested version.
For example, JSON is not supported prior to 1.2.
"""
pass

View File

@@ -0,0 +1,52 @@
# 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.
"""
Exceptions relating to specific conditions that occur when (de)serializing/(de)normalizing CycloneDX BOM.
"""
from . import CycloneDxException
class CycloneDxSerializationException(CycloneDxException):
"""
Base exception that covers all exceptions that may be thrown during model serializing/normalizing.
"""
pass
class CycloneDxDeserializationException(CycloneDxException):
"""
Base exception that covers all exceptions that may be thrown during model deserializing/denormalizing.
"""
pass
class SerializationOfUnsupportedComponentTypeException(CycloneDxSerializationException):
"""
Raised when attempting serializing/normalizing a :py:class:`cyclonedx.model.component.Component`
to a :py:class:`cyclonedx.schema.schema.BaseSchemaVersion`
which does not support that :py:class:`cyclonedx.model.component.ComponentType`
.
"""
class SerializationOfUnexpectedValueException(CycloneDxSerializationException, ValueError):
"""
Raised when attempting serializing/normalizing a type that is not expected there.
"""

View File

@@ -0,0 +1,20 @@
# 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.
"""
Factories used in this library.
"""

View File

@@ -0,0 +1,88 @@
# 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, Optional
from ..exception.factory import InvalidLicenseExpressionException, InvalidSpdxLicenseException
from ..model.license import DisjunctiveLicense, LicenseExpression
from ..spdx import fixup_id as spdx_fixup, is_expression as is_spdx_expression
if TYPE_CHECKING: # pragma: no cover
from ..model import AttachedText, XsUri
from ..model.license import License, LicenseAcknowledgement
class LicenseFactory:
"""Factory for :class:`cyclonedx.model.license.License`."""
def make_from_string(self, value: str, *,
license_text: Optional['AttachedText'] = None,
license_url: Optional['XsUri'] = None,
license_acknowledgement: Optional['LicenseAcknowledgement'] = None
) -> 'License':
"""Make a :class:`cyclonedx.model.license.License` from a string."""
try:
return self.make_with_id(value,
text=license_text,
url=license_url,
acknowledgement=license_acknowledgement)
except InvalidSpdxLicenseException:
pass
try:
return self.make_with_expression(value,
acknowledgement=license_acknowledgement)
except InvalidLicenseExpressionException:
pass
return self.make_with_name(value,
text=license_text,
url=license_url,
acknowledgement=license_acknowledgement)
def make_with_expression(self, expression: str, *,
acknowledgement: Optional['LicenseAcknowledgement'] = None
) -> LicenseExpression:
"""Make a :class:`cyclonedx.model.license.LicenseExpression` with a compound expression.
Utilizes :func:`cyclonedx.spdx.is_expression`.
:raises InvalidLicenseExpressionException: if param `value` is not known/supported license expression
"""
if is_spdx_expression(expression):
return LicenseExpression(expression, acknowledgement=acknowledgement)
raise InvalidLicenseExpressionException(expression)
def make_with_id(self, spdx_id: str, *,
text: Optional['AttachedText'] = None,
url: Optional['XsUri'] = None,
acknowledgement: Optional['LicenseAcknowledgement'] = None
) -> DisjunctiveLicense:
"""Make a :class:`cyclonedx.model.license.DisjunctiveLicense` from an SPDX-ID.
:raises InvalidSpdxLicenseException: if param `spdx_id` was not known/supported SPDX-ID
"""
spdx_license_id = spdx_fixup(spdx_id)
if spdx_license_id is None:
raise InvalidSpdxLicenseException(spdx_id)
return DisjunctiveLicense(id=spdx_license_id, text=text, url=url, acknowledgement=acknowledgement)
def make_with_name(self, name: str, *,
text: Optional['AttachedText'] = None,
url: Optional['XsUri'] = None,
acknowledgement: Optional['LicenseAcknowledgement'] = None
) -> DisjunctiveLicense:
"""Make a :class:`cyclonedx.model.license.DisjunctiveLicense` with a name."""
return DisjunctiveLicense(name=name, text=text, url=url, acknowledgement=acknowledgement)

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -0,0 +1,101 @@
# This file is part of CycloneDX Python Library
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
# Copyright (c) OWASP Foundation. All Rights Reserved.
from typing import TYPE_CHECKING, Any, Optional
import py_serializable as serializable
from ..exception.serialization import CycloneDxDeserializationException, SerializationOfUnexpectedValueException
if TYPE_CHECKING: # pragma: no cover
from typing import Type, TypeVar
_T_BR = TypeVar('_T_BR', bound='BomRef')
@serializable.serializable_class
class BomRef(serializable.helpers.BaseHelper):
"""
An identifier that can be used to reference objects elsewhere in the BOM.
This copies a similar pattern used in the CycloneDX PHP Library.
.. note::
See https://github.com/CycloneDX/cyclonedx-php-library/blob/master/docs/dev/decisions/BomDependencyDataModel.md
"""
def __init__(self, value: Optional[str] = None) -> None:
self.value = value
@property
@serializable.json_name('.')
@serializable.xml_name('.')
def value(self) -> Optional[str]:
return self._value
@value.setter
def value(self, value: Optional[str]) -> None:
# empty strings become `None`
self._value = value or None
def __eq__(self, other: object) -> bool:
return (self is other) or (
isinstance(other, BomRef)
# `None` value is not discriminative in this domain
# see also: `BomRefDiscriminator`
and other._value is not None
and self._value is not None
and other._value == self._value
)
def __lt__(self, other: Any) -> bool:
if isinstance(other, BomRef):
return str(self) < str(other)
return NotImplemented
def __hash__(self) -> int:
return hash(self._value or f'__id__{id(self)}')
def __repr__(self) -> str:
return f'<BomRef {self._value!r} id={id(self)}>'
def __str__(self) -> str:
return self._value or ''
def __bool__(self) -> bool:
return self._value is not None
# region impl BaseHelper
@classmethod
def serialize(cls, o: Any) -> Optional[str]:
if isinstance(o, cls):
return o.value
raise SerializationOfUnexpectedValueException(
f'Attempt to serialize a non-BomRef: {o!r}')
@classmethod
def deserialize(cls: 'Type[_T_BR]', o: Any) -> '_T_BR':
try:
return cls(value=str(o))
except ValueError as err:
raise CycloneDxDeserializationException(
f'BomRef string supplied does not parse: {o!r}'
) from err
# endregion impl BaseHelper

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,386 @@
# This file is part of CycloneDX Python Library
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
# Copyright (c) OWASP Foundation. All Rights Reserved.
from typing import Any, Iterable, Optional, Union
import py_serializable as serializable
from sortedcontainers import SortedSet
from .._internal.bom_ref import bom_ref_from_str as _bom_ref_from_str
from .._internal.compare import ComparableTuple as _ComparableTuple
from ..schema.schema import SchemaVersion1Dot6
from . import XsUri
from .bom_ref import BomRef
@serializable.serializable_class
class PostalAddress:
"""
This is our internal representation of the `postalAddressType` complex type that can be used in multiple places
within a CycloneDX BOM document.
.. note::
See the CycloneDX Schema definition: https://cyclonedx.org/docs/1.6/xml/#type_postalAddressType
"""
def __init__(
self, *,
bom_ref: Optional[Union[str, BomRef]] = None,
country: Optional[str] = None,
region: Optional[str] = None,
locality: Optional[str] = None,
post_office_box_number: Optional[str] = None,
postal_code: Optional[str] = None,
street_address: Optional[str] = None,
) -> None:
self._bom_ref = _bom_ref_from_str(bom_ref, optional=True)
self.country = country
self.region = region
self.locality = locality
self.post_office_box_number = post_office_box_number
self.postal_code = postal_code
self.street_address = street_address
@property
@serializable.json_name('bom-ref')
@serializable.type_mapping(BomRef)
@serializable.xml_attribute()
@serializable.xml_name('bom-ref')
def bom_ref(self) -> Optional[BomRef]:
"""
An optional identifier which can be used to reference the component elsewhere in the BOM. Every bom-ref MUST be
unique within the BOM.
Returns:
`BomRef`
"""
return self._bom_ref
@property
@serializable.xml_sequence(10)
def country(self) -> Optional[str]:
"""
The country name or the two-letter ISO 3166-1 country code.
Returns:
`str` or `None`
"""
return self._country
@country.setter
def country(self, country: Optional[str]) -> None:
self._country = country
@property
@serializable.xml_sequence(20)
def region(self) -> Optional[str]:
"""
The region or state in the country. For example, Texas.
Returns:
`str` or `None`
"""
return self._region
@region.setter
def region(self, region: Optional[str]) -> None:
self._region = region
@property
@serializable.xml_sequence(30)
def locality(self) -> Optional[str]:
"""
The locality or city within the country. For example, Austin.
Returns:
`str` or `None`
"""
return self._locality
@locality.setter
def locality(self, locality: Optional[str]) -> None:
self._locality = locality
@property
@serializable.xml_sequence(40)
def post_office_box_number(self) -> Optional[str]:
"""
The post office box number. For example, 901.
Returns:
`str` or `None`
"""
return self._post_office_box_number
@post_office_box_number.setter
def post_office_box_number(self, post_office_box_number: Optional[str]) -> None:
self._post_office_box_number = post_office_box_number
@property
@serializable.xml_sequence(60)
def postal_code(self) -> Optional[str]:
"""
The postal code. For example, 78758.
Returns:
`str` or `None`
"""
return self._postal_code
@postal_code.setter
def postal_code(self, postal_code: Optional[str]) -> None:
self._postal_code = postal_code
@property
@serializable.xml_sequence(70)
def street_address(self) -> Optional[str]:
"""
The street address. For example, 100 Main Street.
Returns:
`str` or `None`
"""
return self._street_address
@street_address.setter
def street_address(self, street_address: Optional[str]) -> None:
self._street_address = street_address
def __comparable_tuple(self) -> _ComparableTuple:
return _ComparableTuple((
self.country, self.region, self.locality, self.postal_code,
self.post_office_box_number,
self.street_address,
None if self.bom_ref is None else self.bom_ref.value,
))
def __eq__(self, other: object) -> bool:
if isinstance(other, PostalAddress):
return self.__comparable_tuple() == other.__comparable_tuple()
return False
def __lt__(self, other: Any) -> bool:
if isinstance(other, PostalAddress):
return self.__comparable_tuple() < other.__comparable_tuple()
return NotImplemented
def __hash__(self) -> int:
return hash(self.__comparable_tuple())
def __repr__(self) -> str:
return f'<PostalAddress bom-ref={self.bom_ref}, street_address={self.street_address}, country={self.country}>'
@serializable.serializable_class
class OrganizationalContact:
"""
This is our internal representation of the `organizationalContact` complex type that can be used in multiple places
within a CycloneDX BOM document.
.. note::
See the CycloneDX Schema definition: https://cyclonedx.org/docs/1.6/xml/#type_organizationalContact
"""
def __init__(
self, *,
name: Optional[str] = None,
phone: Optional[str] = None,
email: Optional[str] = None,
) -> None:
self.name = name
self.email = email
self.phone = phone
@property
@serializable.xml_sequence(1)
@serializable.xml_string(serializable.XmlStringSerializationType.NORMALIZED_STRING)
def name(self) -> Optional[str]:
"""
Get the name of the contact.
Returns:
`str` if set else `None`
"""
return self._name
@name.setter
def name(self, name: Optional[str]) -> None:
self._name = name
@property
@serializable.xml_sequence(2)
@serializable.xml_string(serializable.XmlStringSerializationType.NORMALIZED_STRING)
def email(self) -> Optional[str]:
"""
Get the email of the contact.
Returns:
`str` if set else `None`
"""
return self._email
@email.setter
def email(self, email: Optional[str]) -> None:
self._email = email
@property
@serializable.xml_sequence(3)
@serializable.xml_string(serializable.XmlStringSerializationType.NORMALIZED_STRING)
def phone(self) -> Optional[str]:
"""
Get the phone of the contact.
Returns:
`str` if set else `None`
"""
return self._phone
@phone.setter
def phone(self, phone: Optional[str]) -> None:
self._phone = phone
def __comparable_tuple(self) -> _ComparableTuple:
return _ComparableTuple((
self.name, self.email, self.phone
))
def __eq__(self, other: object) -> bool:
if isinstance(other, OrganizationalContact):
return self.__comparable_tuple() == other.__comparable_tuple()
return False
def __lt__(self, other: Any) -> bool:
if isinstance(other, OrganizationalContact):
return self.__comparable_tuple() < other.__comparable_tuple()
return NotImplemented
def __hash__(self) -> int:
return hash(self.__comparable_tuple())
def __repr__(self) -> str:
return f'<OrganizationalContact name={self.name}, email={self.email}, phone={self.phone}>'
@serializable.serializable_class
class OrganizationalEntity:
"""
This is our internal representation of the `organizationalEntity` complex type that can be used in multiple places
within a CycloneDX BOM document.
.. note::
See the CycloneDX Schema definition: https://cyclonedx.org/docs/1.6/xml/#type_organizationalEntity
"""
def __init__(
self, *,
name: Optional[str] = None,
urls: Optional[Iterable[XsUri]] = None,
contacts: Optional[Iterable[OrganizationalContact]] = None,
address: Optional[PostalAddress] = None,
) -> None:
self.name = name
self.address = address
self.urls = urls or [] # type:ignore[assignment]
self.contacts = contacts or [] # type:ignore[assignment]
@property
@serializable.xml_sequence(10)
@serializable.xml_string(serializable.XmlStringSerializationType.NORMALIZED_STRING)
def name(self) -> Optional[str]:
"""
Get the name of the organization.
Returns:
`str` if set else `None`
"""
return self._name
@name.setter
def name(self, name: Optional[str]) -> None:
self._name = name
@property
@serializable.view(SchemaVersion1Dot6)
@serializable.xml_sequence(20)
def address(self) -> Optional[PostalAddress]:
"""
The physical address (location) of the organization.
Returns:
`PostalAddress` or `None`
"""
return self._address
@address.setter
def address(self, address: Optional[PostalAddress]) -> None:
self._address = address
@property
@serializable.json_name('url')
@serializable.xml_array(serializable.XmlArraySerializationType.FLAT, 'url')
@serializable.xml_sequence(30)
def urls(self) -> 'SortedSet[XsUri]':
"""
Get a list of URLs of the organization. Multiple URLs are allowed.
Returns:
Set of `XsUri`
"""
return self._urls
@urls.setter
def urls(self, urls: Iterable[XsUri]) -> None:
self._urls = SortedSet(urls)
@property
@serializable.json_name('contact')
@serializable.xml_array(serializable.XmlArraySerializationType.FLAT, 'contact')
@serializable.xml_sequence(40)
def contacts(self) -> 'SortedSet[OrganizationalContact]':
"""
Get a list of contact person at the organization. Multiple contacts are allowed.
Returns:
Set of `OrganizationalContact`
"""
return self._contacts
@contacts.setter
def contacts(self, contacts: Iterable[OrganizationalContact]) -> None:
self._contacts = SortedSet(contacts)
def __comparable_tuple(self) -> _ComparableTuple:
return _ComparableTuple((
self.name, _ComparableTuple(self.urls), _ComparableTuple(self.contacts)
))
def __eq__(self, other: object) -> bool:
if isinstance(other, OrganizationalEntity):
return self.__comparable_tuple() == other.__comparable_tuple()
return False
def __lt__(self, other: Any) -> bool:
if isinstance(other, OrganizationalEntity):
return self.__comparable_tuple() < other.__comparable_tuple()
return NotImplemented
def __hash__(self) -> int:
return hash(self.__comparable_tuple())
def __repr__(self) -> str:
return f'<OrganizationalEntity name={self.name}>'

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -0,0 +1,116 @@
# This file is part of CycloneDX Python Library
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
# Copyright (c) OWASP Foundation. All Rights Reserved.
from abc import ABC, abstractmethod
from typing import Any, Iterable, List, Optional, Set
import py_serializable as serializable
from sortedcontainers import SortedSet
from .._internal.compare import ComparableTuple as _ComparableTuple
from ..exception.serialization import SerializationOfUnexpectedValueException
from .bom_ref import BomRef
class _DependencyRepositorySerializationHelper(serializable.helpers.BaseHelper):
""" THIS CLASS IS NON-PUBLIC API """
@classmethod
def serialize(cls, o: Any) -> List[str]:
if isinstance(o, (SortedSet, set)):
return [str(i.ref) for i in o]
raise SerializationOfUnexpectedValueException(
f'Attempt to serialize a non-DependencyRepository: {o!r}')
@classmethod
def deserialize(cls, o: Any) -> Set['Dependency']:
dependencies = set()
if isinstance(o, list):
for v in o:
dependencies.add(Dependency(ref=BomRef(value=v)))
return dependencies
@serializable.serializable_class
class Dependency:
"""
Models a Dependency within a BOM.
.. note::
See https://cyclonedx.org/docs/1.6/xml/#type_dependencyType
"""
def __init__(self, ref: BomRef, dependencies: Optional[Iterable['Dependency']] = None) -> None:
self.ref = ref
self.dependencies = dependencies or [] # type:ignore[assignment]
@property
@serializable.type_mapping(BomRef)
@serializable.xml_attribute()
def ref(self) -> BomRef:
return self._ref
@ref.setter
def ref(self, ref: BomRef) -> None:
self._ref = ref
@property
@serializable.json_name('dependsOn')
@serializable.type_mapping(_DependencyRepositorySerializationHelper)
@serializable.xml_array(serializable.XmlArraySerializationType.FLAT, 'dependency')
def dependencies(self) -> 'SortedSet[Dependency]':
return self._dependencies
@dependencies.setter
def dependencies(self, dependencies: Iterable['Dependency']) -> None:
self._dependencies = SortedSet(dependencies)
def dependencies_as_bom_refs(self) -> Set[BomRef]:
return set(map(lambda d: d.ref, self.dependencies))
def __comparable_tuple(self) -> _ComparableTuple:
return _ComparableTuple((
self.ref, _ComparableTuple(self.dependencies)
))
def __eq__(self, other: object) -> bool:
if isinstance(other, Dependency):
return self.__comparable_tuple() == other.__comparable_tuple()
return False
def __lt__(self, other: Any) -> bool:
if isinstance(other, Dependency):
return self.__comparable_tuple() < other.__comparable_tuple()
return NotImplemented
def __hash__(self) -> int:
return hash(self.__comparable_tuple())
def __repr__(self) -> str:
return f'<Dependency ref={self.ref!r}, targets={len(self.dependencies)}>'
class Dependable(ABC):
"""
Dependable objects can be part of the Dependency Graph
"""
@property
@abstractmethod
def bom_ref(self) -> BomRef:
... # pragma: no cover

View File

@@ -0,0 +1,106 @@
# This file is part of CycloneDX Python Library
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
# Copyright (c) OWASP Foundation. All Rights Reserved.
"""
This set of classes represents the data about Impact Analysis.
Impact Analysis is new for CycloneDX schema version 1.
.. note::
See the CycloneDX Schema extension definition https://cyclonedx.org/docs/1.6
"""
from enum import Enum
import py_serializable as serializable
@serializable.serializable_enum
class ImpactAnalysisAffectedStatus(str, Enum):
"""
Enum object that defines the permissible impact analysis affected states.
The vulnerability status of a given version or range of versions of a product.
The statuses 'affected' and 'unaffected' indicate that the version is affected or unaffected by the vulnerability.
The status 'unknown' indicates that it is unknown or unspecified whether the given version is affected. There can
be many reasons for an 'unknown' status, including that an investigation has not been undertaken or that a vendor
has not disclosed the status.
.. note::
See the CycloneDX Schema definition: https://cyclonedx.org/docs/1.6/#type_impactAnalysisAffectedStatusType
"""
AFFECTED = 'affected'
UNAFFECTED = 'unaffected'
UNKNOWN = 'unknown'
@serializable.serializable_enum
class ImpactAnalysisJustification(str, Enum):
"""
Enum object that defines the rationale of why the impact analysis state was asserted.
.. note::
See the CycloneDX Schema definition: https://cyclonedx.org/docs/1.6/#type_impactAnalysisJustificationType
"""
CODE_NOT_PRESENT = 'code_not_present'
CODE_NOT_REACHABLE = 'code_not_reachable'
PROTECTED_AT_PERIMITER = 'protected_at_perimeter'
PROTECTED_AT_RUNTIME = 'protected_at_runtime'
PROTECTED_BY_COMPILER = 'protected_by_compiler'
PROTECTED_BY_MITIGATING_CONTROL = 'protected_by_mitigating_control'
REQUIRES_CONFIGURATION = 'requires_configuration'
REQUIRES_DEPENDENCY = 'requires_dependency'
REQUIRES_ENVIRONMENT = 'requires_environment'
@serializable.serializable_enum
class ImpactAnalysisResponse(str, Enum):
"""
Enum object that defines the valid rationales as to why the impact analysis state was asserted.
.. note::
See the CycloneDX Schema definition: https://cyclonedx.org/docs/1.6/#type_impactAnalysisResponsesType
"""
CAN_NOT_FIX = 'can_not_fix'
ROLLBACK = 'rollback'
UPDATE = 'update'
WILL_NOT_FIX = 'will_not_fix'
WORKAROUND_AVAILABLE = 'workaround_available'
@serializable.serializable_enum
class ImpactAnalysisState(str, Enum):
"""
Enum object that defines the permissible impact analysis states.
.. note::
See the CycloneDX Schema definition: https://cyclonedx.org/docs/1.6/#type_impactAnalysisStateType
"""
RESOLVED = 'resolved'
RESOLVED_WITH_PEDIGREE = 'resolved_with_pedigree'
EXPLOITABLE = 'exploitable'
IN_TRIAGE = 'in_triage'
FALSE_POSITIVE = 'false_positive'
NOT_AFFECTED = 'not_affected'

View File

@@ -0,0 +1,250 @@
# This file is part of CycloneDX Python Library
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
# Copyright (c) OWASP Foundation. All Rights Reserved.
from enum import Enum
from typing import Any, Iterable, Optional
import py_serializable as serializable
from sortedcontainers import SortedSet
from .._internal.compare import ComparableTuple as _ComparableTuple
from . import XsUri
@serializable.serializable_enum
class IssueClassification(str, Enum):
"""
This is our internal representation of the enum `issueClassification`.
.. note::
See the CycloneDX Schema definition: https://cyclonedx.org/docs/1.6/xml/#type_issueClassification
"""
DEFECT = 'defect'
ENHANCEMENT = 'enhancement'
SECURITY = 'security'
@serializable.serializable_class
class IssueTypeSource:
"""
This is our internal representation ofa source within the IssueType complex type that can be used in multiple
places within a CycloneDX BOM document.
.. note::
See the CycloneDX Schema definition: https://cyclonedx.org/docs/1.6/xml/#type_issueType
"""
def __init__(
self, *,
name: Optional[str] = None,
url: Optional[XsUri] = None,
) -> None:
self.name = name
self.url = url
@property
@serializable.xml_string(serializable.XmlStringSerializationType.NORMALIZED_STRING)
def name(self) -> Optional[str]:
"""
The name of the source. For example "National Vulnerability Database", "NVD", and "Apache".
Returns:
`str` if set else `None`
"""
return self._name
@name.setter
def name(self, name: Optional[str]) -> None:
self._name = name
@property
def url(self) -> Optional[XsUri]:
"""
Optional url of the issue documentation as provided by the source.
Returns:
`XsUri` if set else `None`
"""
return self._url
@url.setter
def url(self, url: Optional[XsUri]) -> None:
self._url = url
def __comparable_tuple(self) -> _ComparableTuple:
return _ComparableTuple((
self.name, self.url
))
def __eq__(self, other: object) -> bool:
if isinstance(other, IssueTypeSource):
return self.__comparable_tuple() == other.__comparable_tuple()
return False
def __lt__(self, other: Any) -> bool:
if isinstance(other, IssueTypeSource):
return self.__comparable_tuple() < other.__comparable_tuple()
return NotImplemented
def __hash__(self) -> int:
return hash(self.__comparable_tuple())
def __repr__(self) -> str:
return f'<IssueTypeSource name={self._name}, url={self.url}>'
@serializable.serializable_class
class IssueType:
"""
This is our internal representation of an IssueType complex type that can be used in multiple places within
a CycloneDX BOM document.
.. note::
See the CycloneDX Schema definition: https://cyclonedx.org/docs/1.6/xml/#type_issueType
"""
def __init__(
self, *,
type: IssueClassification,
id: Optional[str] = None,
name: Optional[str] = None,
description: Optional[str] = None,
source: Optional[IssueTypeSource] = None,
references: Optional[Iterable[XsUri]] = None,
) -> None:
self.type = type
self.id = id
self.name = name
self.description = description
self.source = source
self.references = references or [] # type:ignore[assignment]
@property
@serializable.xml_attribute()
def type(self) -> IssueClassification:
"""
Specifies the type of issue.
Returns:
`IssueClassification`
"""
return self._type
@type.setter
def type(self, type: IssueClassification) -> None:
self._type = type
@property
@serializable.xml_sequence(1)
@serializable.xml_string(serializable.XmlStringSerializationType.NORMALIZED_STRING)
def id(self) -> Optional[str]:
"""
The identifier of the issue assigned by the source of the issue.
Returns:
`str` if set else `None`
"""
return self._id
@id.setter
def id(self, id: Optional[str]) -> None:
self._id = id
@property
@serializable.xml_sequence(2)
@serializable.xml_string(serializable.XmlStringSerializationType.NORMALIZED_STRING)
def name(self) -> Optional[str]:
"""
The name of the issue.
Returns:
`str` if set else `None`
"""
return self._name
@name.setter
def name(self, name: Optional[str]) -> None:
self._name = name
@property
@serializable.xml_sequence(3)
@serializable.xml_string(serializable.XmlStringSerializationType.NORMALIZED_STRING)
def description(self) -> Optional[str]:
"""
A description of the issue.
Returns:
`str` if set else `None`
"""
return self._description
@description.setter
def description(self, description: Optional[str]) -> None:
self._description = description
@property
@serializable.xml_sequence(4)
def source(self) -> Optional[IssueTypeSource]:
"""
The source of this issue.
Returns:
`IssueTypeSource` if set else `None`
"""
return self._source
@source.setter
def source(self, source: Optional[IssueTypeSource]) -> None:
self._source = source
@property
@serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'url')
@serializable.xml_sequence(5)
def references(self) -> 'SortedSet[XsUri]':
"""
Any reference URLs related to this issue.
Returns:
Set of `XsUri`
"""
return self._references
@references.setter
def references(self, references: Iterable[XsUri]) -> None:
self._references = SortedSet(references)
def __comparable_tuple(self) -> _ComparableTuple:
return _ComparableTuple((
self.type, self.id, self.name, self.description, self.source,
_ComparableTuple(self.references)
))
def __eq__(self, other: object) -> bool:
if isinstance(other, IssueType):
return self.__comparable_tuple() == other.__comparable_tuple()
return False
def __lt__(self, other: Any) -> bool:
if isinstance(other, IssueType):
return self.__comparable_tuple() < other.__comparable_tuple()
return NotImplemented
def __hash__(self) -> int:
return hash(self.__comparable_tuple())
def __repr__(self) -> str:
return f'<IssueType type={self.type}, id={self.id}, name={self.name}>'

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,176 @@
# 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.
"""
Set of classes and methods for outputting our libraries internal Bom model to CycloneDX documents in varying formats
and according to different versions of the CycloneDX schema standard.
"""
import os
from abc import ABC, abstractmethod
from itertools import chain
from random import random
from typing import TYPE_CHECKING, Any, Iterable, Literal, Mapping, Optional, Type, Union, overload
from ..schema import OutputFormat, SchemaVersion
if TYPE_CHECKING: # pragma: no cover
from ..model.bom import Bom
from ..model.bom_ref import BomRef
from .json import Json as JsonOutputter
from .xml import Xml as XmlOutputter
class BaseOutput(ABC):
def __init__(self, bom: 'Bom', **kwargs: int) -> None:
super().__init__(**kwargs)
self._bom = bom
self._generated: bool = False
@property
@abstractmethod
def schema_version(self) -> SchemaVersion:
... # pragma: no cover
@property
@abstractmethod
def output_format(self) -> OutputFormat:
... # pragma: no cover
@property
def generated(self) -> bool:
return self._generated
@generated.setter
def generated(self, generated: bool) -> None:
self._generated = generated
def get_bom(self) -> 'Bom':
return self._bom
def set_bom(self, bom: 'Bom') -> None:
self._bom = bom
@abstractmethod
def generate(self, force_regeneration: bool = False) -> None:
... # pragma: no cover
@abstractmethod
def output_as_string(self, *,
indent: Optional[Union[int, str]] = None,
**kwargs: Any) -> str:
... # pragma: no cover
def output_to_file(self, filename: str, allow_overwrite: bool = False, *,
indent: Optional[Union[int, str]] = None,
**kwargs: Any) -> None:
# Check directory writable
output_filename = os.path.realpath(filename)
output_directory = os.path.dirname(output_filename)
if not os.access(output_directory, os.W_OK):
raise PermissionError(output_directory)
if os.path.exists(output_filename) and not allow_overwrite:
raise FileExistsError(output_filename)
with open(output_filename, mode='wb') as f_out:
f_out.write(self.output_as_string(indent=indent).encode('utf-8'))
@overload
def make_outputter(bom: 'Bom', output_format: Literal[OutputFormat.JSON],
schema_version: SchemaVersion) -> 'JsonOutputter':
... # pragma: no cover
@overload
def make_outputter(bom: 'Bom', output_format: Literal[OutputFormat.XML],
schema_version: SchemaVersion) -> 'XmlOutputter':
... # pragma: no cover
@overload
def make_outputter(bom: 'Bom', output_format: OutputFormat,
schema_version: SchemaVersion) -> Union['XmlOutputter', 'JsonOutputter']:
... # pragma: no cover
def make_outputter(bom: 'Bom', output_format: OutputFormat, schema_version: SchemaVersion) -> BaseOutput:
"""
Helper method to quickly get the correct output class/formatter.
Pass in your BOM and optionally an output format and schema version (defaults to XML and latest schema version).
Raises error when no instance could be made.
:param bom: Bom
:param output_format: OutputFormat
:param schema_version: SchemaVersion
:return: BaseOutput
"""
if TYPE_CHECKING: # pragma: no cover
BY_SCHEMA_VERSION: Mapping[SchemaVersion, Type[BaseOutput]] # noqa:N806
if OutputFormat.JSON is output_format:
from .json import BY_SCHEMA_VERSION
elif OutputFormat.XML is output_format:
from .xml import BY_SCHEMA_VERSION
else:
raise ValueError(f'Unexpected output_format: {output_format!r}')
klass = BY_SCHEMA_VERSION.get(schema_version, None)
if klass is None:
raise ValueError(f'Unknown {output_format.name}/schema_version: {schema_version!r}')
return klass(bom)
class BomRefDiscriminator:
def __init__(self, bomrefs: Iterable['BomRef'], prefix: str = 'BomRef') -> None:
# do not use dict/set here, different BomRefs with same value have same hash and would shadow each other
self._bomrefs = tuple((bomref, bomref.value) for bomref in bomrefs)
self._prefix = prefix
def __enter__(self) -> None:
self.discriminate()
def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
self.reset()
def discriminate(self) -> None:
known_values = []
for bomref, _ in self._bomrefs:
value = bomref.value
if value is None or value in known_values:
value = self._make_unique()
bomref.value = value
known_values.append(value)
def reset(self) -> None:
for bomref, original_value in self._bomrefs:
bomref.value = original_value
def _make_unique(self) -> str:
return f'{self._prefix}{str(random())[1:]}{str(random())[1:]}' # nosec B311
@classmethod
def from_bom(cls, bom: 'Bom', prefix: str = 'BomRef') -> 'BomRefDiscriminator':
return cls(chain(
map(lambda c: c.bom_ref, bom._get_all_components()),
map(lambda s: s.bom_ref, bom.services),
map(lambda v: v.bom_ref, bom.vulnerabilities)
), prefix)

View File

@@ -0,0 +1,142 @@
# 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 abstractmethod
from json import dumps as json_dumps, loads as json_loads
from typing import TYPE_CHECKING, Any, Dict, Literal, Optional, Type, Union
from ..exception.output import FormatNotSupportedException
from ..schema import OutputFormat, SchemaVersion
from ..schema.schema import (
SCHEMA_VERSIONS,
BaseSchemaVersion,
SchemaVersion1Dot0,
SchemaVersion1Dot1,
SchemaVersion1Dot2,
SchemaVersion1Dot3,
SchemaVersion1Dot4,
SchemaVersion1Dot5,
SchemaVersion1Dot6,
)
from . import BaseOutput, BomRefDiscriminator
if TYPE_CHECKING: # pragma: no cover
from ..model.bom import Bom
class Json(BaseOutput, BaseSchemaVersion):
def __init__(self, bom: 'Bom') -> None:
super().__init__(bom=bom)
self._bom_json: Dict[str, Any] = dict()
@property
def schema_version(self) -> SchemaVersion:
return self.schema_version_enum
@property
def output_format(self) -> Literal[OutputFormat.JSON]:
return OutputFormat.JSON
def generate(self, force_regeneration: bool = False) -> None:
if self.generated and not force_regeneration:
return
schema_uri: Optional[str] = self._get_schema_uri()
if not schema_uri:
raise FormatNotSupportedException(
f'JSON is not supported by CycloneDX in schema version {self.schema_version.to_version()}')
_json_core = {
'$schema': schema_uri,
'bomFormat': 'CycloneDX',
'specVersion': self.schema_version.to_version()
}
_view = SCHEMA_VERSIONS.get(self.schema_version_enum)
bom = self.get_bom()
bom.validate()
with BomRefDiscriminator.from_bom(bom):
bom_json: Dict[str, Any] = json_loads(
bom.as_json( # type:ignore[attr-defined]
view_=_view))
bom_json.update(_json_core)
self._bom_json = bom_json
self.generated = True
def output_as_string(self, *,
indent: Optional[Union[int, str]] = None,
**kwargs: Any) -> str:
self.generate()
return json_dumps(self._bom_json,
indent=indent)
@abstractmethod
def _get_schema_uri(self) -> Optional[str]:
... # pragma: no cover
class JsonV1Dot0(Json, SchemaVersion1Dot0):
def _get_schema_uri(self) -> None:
return None
class JsonV1Dot1(Json, SchemaVersion1Dot1):
def _get_schema_uri(self) -> None:
return None
class JsonV1Dot2(Json, SchemaVersion1Dot2):
def _get_schema_uri(self) -> str:
return 'http://cyclonedx.org/schema/bom-1.2b.schema.json'
class JsonV1Dot3(Json, SchemaVersion1Dot3):
def _get_schema_uri(self) -> str:
return 'http://cyclonedx.org/schema/bom-1.3a.schema.json'
class JsonV1Dot4(Json, SchemaVersion1Dot4):
def _get_schema_uri(self) -> str:
return 'http://cyclonedx.org/schema/bom-1.4.schema.json'
class JsonV1Dot5(Json, SchemaVersion1Dot5):
def _get_schema_uri(self) -> str:
return 'http://cyclonedx.org/schema/bom-1.5.schema.json'
class JsonV1Dot6(Json, SchemaVersion1Dot6):
def _get_schema_uri(self) -> str:
return 'http://cyclonedx.org/schema/bom-1.6.schema.json'
BY_SCHEMA_VERSION: Dict[SchemaVersion, Type[Json]] = {
SchemaVersion.V1_6: JsonV1Dot6,
SchemaVersion.V1_5: JsonV1Dot5,
SchemaVersion.V1_4: JsonV1Dot4,
SchemaVersion.V1_3: JsonV1Dot3,
SchemaVersion.V1_2: JsonV1Dot2,
SchemaVersion.V1_1: JsonV1Dot1,
SchemaVersion.V1_0: JsonV1Dot0,
}

View File

@@ -0,0 +1,135 @@
# 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, Dict, Literal, Optional, Type, Union
from xml.dom.minidom import parseString as dom_parseString # nosec B408
from xml.etree.ElementTree import Element as XmlElement, tostring as xml_dumps # nosec B405
from ..schema import OutputFormat, SchemaVersion
from ..schema.schema import (
SCHEMA_VERSIONS,
BaseSchemaVersion,
SchemaVersion1Dot0,
SchemaVersion1Dot1,
SchemaVersion1Dot2,
SchemaVersion1Dot3,
SchemaVersion1Dot4,
SchemaVersion1Dot5,
SchemaVersion1Dot6,
)
from . import BaseOutput, BomRefDiscriminator
if TYPE_CHECKING: # pragma: no cover
from ..model.bom import Bom
class Xml(BaseSchemaVersion, BaseOutput):
def __init__(self, bom: 'Bom') -> None:
super().__init__(bom=bom)
self._bom_xml: str = ''
@property
def schema_version(self) -> SchemaVersion:
return self.schema_version_enum
@property
def output_format(self) -> Literal[OutputFormat.XML]:
return OutputFormat.XML
def generate(self, force_regeneration: bool = False) -> None:
if self.generated and not force_regeneration:
return
_view = SCHEMA_VERSIONS[self.schema_version_enum]
bom = self.get_bom()
bom.validate()
xmlns = self.get_target_namespace()
with BomRefDiscriminator.from_bom(bom):
self._bom_xml = '<?xml version="1.0" ?>\n' + xml_dumps(
bom.as_xml( # type:ignore[attr-defined]
_view, as_string=False, xmlns=xmlns),
method='xml', default_namespace=xmlns, encoding='unicode',
# `xml-declaration` is inconsistent/bugged in py38,
# especially on Windows it will print a non-UTF8 codepage.
# Furthermore, it might add an encoding of "utf-8" which is redundant default value of XML.
# -> so we write the declaration manually, as long as py38 is supported.
xml_declaration=False)
self.generated = True
@staticmethod
def __make_indent(v: Optional[Union[int, str]]) -> str:
if isinstance(v, int):
return ' ' * v
if isinstance(v, str):
return v
return ''
def output_as_string(self, *,
indent: Optional[Union[int, str]] = None,
**kwargs: Any) -> str:
self.generate()
return self._bom_xml if indent is None else dom_parseString( # nosecc B318
self._bom_xml).toprettyxml(
indent=self.__make_indent(indent)
# do not set `encoding` - this would convert result to binary, not string
)
def get_target_namespace(self) -> str:
return f'http://cyclonedx.org/schema/bom/{self.get_schema_version()}'
class XmlV1Dot0(Xml, SchemaVersion1Dot0):
def _create_bom_element(self) -> XmlElement:
return XmlElement('bom', {'xmlns': self.get_target_namespace(), 'version': '1'})
class XmlV1Dot1(Xml, SchemaVersion1Dot1):
pass
class XmlV1Dot2(Xml, SchemaVersion1Dot2):
pass
class XmlV1Dot3(Xml, SchemaVersion1Dot3):
pass
class XmlV1Dot4(Xml, SchemaVersion1Dot4):
pass
class XmlV1Dot5(Xml, SchemaVersion1Dot5):
pass
class XmlV1Dot6(Xml, SchemaVersion1Dot6):
pass
BY_SCHEMA_VERSION: Dict[SchemaVersion, Type[Xml]] = {
SchemaVersion.V1_6: XmlV1Dot6,
SchemaVersion.V1_5: XmlV1Dot5,
SchemaVersion.V1_4: XmlV1Dot4,
SchemaVersion.V1_3: XmlV1Dot3,
SchemaVersion.V1_2: XmlV1Dot2,
SchemaVersion.V1_1: XmlV1Dot1,
SchemaVersion.V1_0: XmlV1Dot0,
}

View File

@@ -0,0 +1,2 @@
# Marker file for PEP 561. This package uses inline types.
# This file is needed to allow other packages to type-check their code against this package.

View File

@@ -0,0 +1,106 @@
# This file is part of CycloneDX Python Library
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
# Copyright (c) OWASP Foundation. All Rights Reserved.
from enum import Enum, auto, unique
from typing import Any, Type, TypeVar
@unique
class OutputFormat(Enum):
"""Output formats.
Cases are hashable.
Do not rely on the actual/literal values, just use enum cases, like so:
my_of = OutputFormat.XML
"""
JSON = auto()
XML = auto()
def __hash__(self) -> int:
return hash(self.name)
def __eq__(self, other: Any) -> bool:
return self is other
_SV = TypeVar('_SV', bound='SchemaVersion')
@unique
class SchemaVersion(Enum):
"""
Schema version.
Cases are hashable.
Cases are comparable(!=,>=,>,==,<,<=)
Do not rely on the actual/literal values, just use enum cases, like so:
my_sv = SchemaVersion.V1_3
"""
V1_6 = (1, 6)
V1_5 = (1, 5)
V1_4 = (1, 4)
V1_3 = (1, 3)
V1_2 = (1, 2)
V1_1 = (1, 1)
V1_0 = (1, 0)
@classmethod
def from_version(cls: Type[_SV], version: str) -> _SV:
"""Return instance based of a version string - e.g. `1.4`"""
return cls(tuple(map(int, version.split('.')))[:2])
def to_version(self) -> str:
"""Return as a version string - e.g. `1.4`"""
return '.'.join(map(str, self.value))
def __ne__(self, other: Any) -> bool:
if isinstance(other, self.__class__):
return self.value != other.value
return NotImplemented # pragma: no cover
def __lt__(self, other: Any) -> bool:
if isinstance(other, self.__class__):
return self.value < other.value
return NotImplemented # pragma: no cover
def __le__(self, other: Any) -> bool:
if isinstance(other, self.__class__):
return self.value <= other.value
return NotImplemented # pragma: no cover
def __eq__(self, other: Any) -> bool:
if isinstance(other, self.__class__):
return self.value == other.value
return NotImplemented # pragma: no cover
def __ge__(self, other: Any) -> bool:
if isinstance(other, self.__class__):
return self.value >= other.value
return NotImplemented # pragma: no cover
def __gt__(self, other: Any) -> bool:
if isinstance(other, self.__class__):
return self.value > other.value
return NotImplemented # pragma: no cover
def __hash__(self) -> int:
return hash(self.name)

View File

@@ -0,0 +1,34 @@
# Resources: Schema files
some schema for offline use as download via [script](../../../tools/schema-downloader.py).
original sources: <https://github.com/CycloneDX/specification/tree/master/schema>
Currently using version
[8a27bfd1be5be0dcb2c208a34d2f4fa0b6d75bd7](https://github.com/CycloneDX/specification/commit/8a27bfd1be5be0dcb2c208a34d2f4fa0b6d75bd7)
| file | note |
|------|------|
| [`bom-1.0.SNAPSHOT.xsd`](bom-1.0.SNAPSHOT.xsd) | applied changes: 1 |
| [`bom-1.1.SNAPSHOT.xsd`](bom-1.1.SNAPSHOT.xsd) | applied changes: 1 |
| [`bom-1.2.SNAPSHOT.xsd`](bom-1.2.SNAPSHOT.xsd) | applied changes: 1 |
| [`bom-1.3.SNAPSHOT.xsd`](bom-1.3.SNAPSHOT.xsd) | applied changes: 1 |
| [`bom-1.4.SNAPSHOT.xsd`](bom-1.4.SNAPSHOT.xsd) | applied changes: 1 |
| [`bom-1.5.SNAPSHOT.xsd`](bom-1.5.SNAPSHOT.xsd) | applied changes: 1 |
| [`bom-1.6.SNAPSHOT.xsd`](bom-1.6.SNAPSHOT.xsd) | applied changes: 1 |
| [`bom-1.2.SNAPSHOT.schema.json`](bom-1.2.SNAPSHOT.schema.json) | applied changes: 2,3,4,5 |
| [`bom-1.3.SNAPSHOT.schema.json`](bom-1.3.SNAPSHOT.schema.json) | applied changes: 2,3,4,5 |
| [`bom-1.4.SNAPSHOT.schema.json`](bom-1.4.SNAPSHOT.schema.json) | applied changes: 2,3,4,5 |
| [`bom-1.5.SNAPSHOT.schema.json`](bom-1.5.SNAPSHOT.schema.json) | applied changes: 2,3,4,5 |
| [`bom-1.6.SNAPSHOT.schema.json`](bom-1.6.SNAPSHOT.schema.json) | applied changes: 2,3,4,5 |
| [`bom-1.2-strict.SNAPSHOT.schema.json`](bom-1.2-strict.SNAPSHOT.schema.json) | applied changes: 2,3,4,5 |
| [`bom-1.3-strict.SNAPSHOT.schema.json`](bom-1.3-strict.SNAPSHOT.schema.json) | applied changes: 2,3,4,5 |
| [`spdx.SNAPSHOT.xsd`](spdx.SNAPSHOT.xsd) | |
| [`spdx.SNAPSHOT.schema.json`](spdx.SNAPSHOT.schema.json) | |
| [`jsf-0.82.SNAPSHOT.schema.json`](jsf-0.82.SNAPSHOT.schema.json) | |
changes:
1. `https?://cyclonedx.org/schema/spdx` was replaced with `spdx.SNAPSHOT.xsd`
2. `spdx.schema.json` was replaced with `spdx.SNAPSHOT.schema.json`
3. `jsf-0.82.schema.json` was replaced with `jsf-0.82.SNAPSHOT.schema.json`
4. `properties.$schema.enum` was removed
5. `required.version` removed, as it is actually optional with default value

View File

@@ -0,0 +1,68 @@
# 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.
"""
Content in here is internal, not for public use.
Breaking changes without notice may happen.
"""
from os.path import dirname, join
from typing import Dict, Optional
from .. import SchemaVersion
__DIR = dirname(__file__)
BOM_XML: Dict[SchemaVersion, Optional[str]] = {
SchemaVersion.V1_6: join(__DIR, 'bom-1.6.SNAPSHOT.xsd'),
SchemaVersion.V1_5: join(__DIR, 'bom-1.5.SNAPSHOT.xsd'),
SchemaVersion.V1_4: join(__DIR, 'bom-1.4.SNAPSHOT.xsd'),
SchemaVersion.V1_3: join(__DIR, 'bom-1.3.SNAPSHOT.xsd'),
SchemaVersion.V1_2: join(__DIR, 'bom-1.2.SNAPSHOT.xsd'),
SchemaVersion.V1_1: join(__DIR, 'bom-1.1.SNAPSHOT.xsd'),
SchemaVersion.V1_0: join(__DIR, 'bom-1.0.SNAPSHOT.xsd'),
}
BOM_JSON: Dict[SchemaVersion, Optional[str]] = {
SchemaVersion.V1_6: join(__DIR, 'bom-1.6.SNAPSHOT.schema.json'),
SchemaVersion.V1_5: join(__DIR, 'bom-1.5.SNAPSHOT.schema.json'),
SchemaVersion.V1_4: join(__DIR, 'bom-1.4.SNAPSHOT.schema.json'),
SchemaVersion.V1_3: join(__DIR, 'bom-1.3.SNAPSHOT.schema.json'),
SchemaVersion.V1_2: join(__DIR, 'bom-1.2.SNAPSHOT.schema.json'),
# <= v1.1 is not defined in JSON
SchemaVersion.V1_1: None,
SchemaVersion.V1_0: None,
}
BOM_JSON_STRICT: Dict[SchemaVersion, Optional[str]] = {
SchemaVersion.V1_6: BOM_JSON[SchemaVersion.V1_6],
SchemaVersion.V1_5: BOM_JSON[SchemaVersion.V1_5],
SchemaVersion.V1_4: BOM_JSON[SchemaVersion.V1_4],
# <= 1.3 need special files
SchemaVersion.V1_3: join(__DIR, 'bom-1.3-strict.SNAPSHOT.schema.json'),
SchemaVersion.V1_2: join(__DIR, 'bom-1.2-strict.SNAPSHOT.schema.json'),
# <= v1.1 is not defined in JSON
SchemaVersion.V1_1: None,
SchemaVersion.V1_0: None,
}
SPDX_JSON = join(__DIR, 'spdx.SNAPSHOT.schema.json')
SPDX_XML = join(__DIR, 'spdx.SNAPSHOT.xsd')
JSF = join(__DIR, 'jsf-0.82.SNAPSHOT.schema.json')

View File

@@ -0,0 +1,247 @@
<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:vc="http://www.w3.org/2007/XMLSchema-versioning"
xmlns:bom="http://cyclonedx.org/schema/bom/1.0"
xmlns:spdx="http://cyclonedx.org/schema/spdx"
elementFormDefault="qualified"
targetNamespace="http://cyclonedx.org/schema/bom/1.0"
vc:minVersion="1.0"
vc:maxVersion="1.1"
version="1.0.1">
<xs:import namespace="http://cyclonedx.org/schema/spdx" schemaLocation="spdx.SNAPSHOT.xsd"/>
<xs:complexType name="component">
<xs:sequence>
<xs:element name="publisher" type="xs:normalizedString" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation>The person(s) or organization(s) that published the component</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="group" type="xs:normalizedString" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation>The grouping name or identifier. This will often be a shortened, single
name of the company or project that produced the component, or the source package or
domain name. Whitespace and special characters should be avoided. Examples include:
apache, org.apache.commons, and apache.org.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="name" type="xs:normalizedString" minOccurs="1" maxOccurs="1">
<xs:annotation>
<xs:documentation>The name of the component. This will often be a shortened, single name
of the component. Examples: commons-lang3 and jquery</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="version" type="xs:normalizedString" minOccurs="1" maxOccurs="1">
<xs:annotation>
<xs:documentation>The component version. The version should ideally comply with semantic versioning
but is not enforced.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="description" type="xs:normalizedString" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation>Specifies a description for the component</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="scope" type="bom:scope" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation>Specifies the scope of the component. If scope is not specified, 'runtime'
scope will be assumed.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="hashes" minOccurs="0" maxOccurs="1">
<xs:complexType>
<xs:sequence minOccurs="0" maxOccurs="unbounded">
<xs:element name="hash" type="bom:hashType"/>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="licenses" minOccurs="0" maxOccurs="1">
<xs:complexType>
<xs:sequence minOccurs="1" maxOccurs="unbounded">
<xs:element name="license">
<xs:complexType>
<xs:sequence>
<xs:choice>
<xs:element name="id" type="spdx:licenseId" minOccurs="0" maxOccurs="unbounded">
<xs:annotation>
<xs:documentation>A valid SPDX license ID</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="name" type="xs:normalizedString" minOccurs="0" maxOccurs="unbounded">
<xs:annotation>
<xs:documentation>If SPDX does not define the license used, this field may be used to provide the license name</xs:documentation>
</xs:annotation>
</xs:element>
</xs:choice>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="copyright" type="xs:normalizedString" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation>An optional copyright notice informing users of the underlying claims to copyright ownership in a published work.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="cpe" type="bom:cpe" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation>Specifies a well-formed CPE name. See https://nvd.nist.gov/products/cpe</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="purl" type="xs:anyURI" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation>
Specifies the package-url (PURL). The purl, if specified, must be valid and conform
to the specification defined at: https://github.com/package-url/purl-spec
</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="modified" type="xs:boolean" minOccurs="1" maxOccurs="1">
<xs:annotation>
<xs:documentation>
A boolean value indicating is the component has been modified from the original.
A value of true indicates the component is a derivative of the original.
A value of false indicates the component has not been modified from the original.
</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="components" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation>
Specifies optional sub-components. This is not a dependency tree. It simply provides
an optional way to group large sets of components together.
</xs:documentation>
</xs:annotation>
<xs:complexType>
<xs:sequence minOccurs="0" maxOccurs="unbounded">
<xs:element name="component" type="bom:component"/>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:sequence>
<xs:attribute name="type" type="bom:classification" use="required">
<xs:annotation>
<xs:documentation>
Specifies the type of component. Software applications, libraries, frameworks, and
other dependencies should be classified as 'application'.
</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:anyAttribute namespace="##other" processContents="lax">
<xs:annotation>
<xs:documentation>User-defined attributes may be used on this element as long as they
do not have the same name as an existing attribute used by the schema.</xs:documentation>
</xs:annotation>
</xs:anyAttribute>
</xs:complexType>
<xs:complexType name="hashType">
<xs:annotation>
<xs:documentation>Specifies the file hash of the component</xs:documentation>
</xs:annotation>
<xs:simpleContent>
<xs:extension base="bom:hashValue">
<xs:attribute name="alg" type="bom:hashAlg" use="required">
<xs:annotation>
<xs:documentation>Specifies the algorithm used to create hash</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
<xs:simpleType name="scope">
<xs:restriction base="xs:string">
<xs:enumeration value="required">
<xs:annotation>
<xs:documentation>The component is required for runtime</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="optional">
<xs:annotation>
<xs:documentation>The component is optional at runtime. Optional components are components that
are not capable of being called due to them not be installed or otherwise accessible by any means.
Components that are installed but due to configuration or other restrictions are prohibited from
being called must be scoped as 'required'.</xs:documentation>
</xs:annotation>
</xs:enumeration>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="classification">
<xs:restriction base="xs:string">
<xs:enumeration value="application"/>
<xs:enumeration value="framework"/>
<xs:enumeration value="library"/>
<xs:enumeration value="operating-system"/>
<xs:enumeration value="device"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="hashAlg">
<xs:restriction base="xs:string">
<xs:enumeration value="MD5"/>
<xs:enumeration value="SHA-1"/>
<xs:enumeration value="SHA-256"/>
<xs:enumeration value="SHA-384"/>
<xs:enumeration value="SHA-512"/>
<xs:enumeration value="SHA3-256"/>
<xs:enumeration value="SHA3-512"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="hashValue">
<xs:restriction base="xs:token">
<xs:pattern value="([a-fA-F0-9]{32})|([a-fA-F0-9]{40})|([a-fA-F0-9]{64})|([a-fA-F0-9]{96})|([a-fA-F0-9]{128})"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="cpe">
<xs:annotation>
<xs:documentation xml:lang="en">
Define the format for acceptable CPE URIs. Supports CPE 2.2 and CPE 2.3 formats. Refer to https://nvd.nist.gov/products/cpe for official specification.
</xs:documentation>
</xs:annotation>
<xs:restriction base="xs:string">
<xs:pattern value="([c][pP][eE]:/[AHOaho]?(:[A-Za-z0-9\._\-~%]*){0,6})|(cpe:2\.3:[aho\*\-](:(((\?*|\*?)([a-zA-Z0-9\-\._]|(\\[\\\*\?!&quot;#$$%&amp;'\(\)\+,/:;&lt;=&gt;@\[\]\^`\{\|}~]))+(\?*|\*?))|[\*\-])){5}(:(([a-zA-Z]{2,3}(-([a-zA-Z]{2}|[0-9]{3}))?)|[\*\-]))(:(((\?*|\*?)([a-zA-Z0-9\-\._]|(\\[\\\*\?!&quot;#$$%&amp;'\(\)\+,/:;&lt;=&gt;@\[\]\^`\{\|}~]))+(\?*|\*?))|[\*\-])){4})"/>
</xs:restriction>
</xs:simpleType>
<xs:element name="bom">
<xs:complexType>
<xs:sequence>
<xs:element name="components">
<xs:complexType>
<xs:sequence minOccurs="0" maxOccurs="unbounded">
<xs:element name="component" type="bom:component"/>
</xs:sequence>
<xs:anyAttribute namespace="##other" processContents="lax">
<xs:annotation>
<xs:documentation>User-defined attributes may be used on this element as long as they
do not have the same name as an existing attribute used by the schema.</xs:documentation>
</xs:annotation>
</xs:anyAttribute>
</xs:complexType>
</xs:element>
</xs:sequence>
<xs:attribute name="version" type="xs:integer" default="1">
<xs:annotation>
<xs:documentation>The version allows component publishers/authors to make changes to existing
BOMs to update various aspects of the document such as description or licenses. When a system
is presented with multiiple BOMs for the same component, the system should use the most recent
version of the BOM. The default version is '1' and should be incremented for each version of the
BOM that is published. Each version of a component should have a unique BOM and if no changes are
made to the BOMs, then each BOM will have a version of '1'.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:anyAttribute namespace="##other" processContents="lax">
<xs:annotation>
<xs:documentation>User-defined attributes may be used on this element as long as they
do not have the same name as an existing attribute used by the schema.</xs:documentation>
</xs:annotation>
</xs:anyAttribute>
</xs:complexType>
</xs:element>
</xs:schema>

View File

@@ -0,0 +1,738 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
CycloneDX Software Bill-of-Material (SBoM) Specification
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.
-->
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:vc="http://www.w3.org/2007/XMLSchema-versioning"
xmlns:bom="http://cyclonedx.org/schema/bom/1.1"
xmlns:spdx="http://cyclonedx.org/schema/spdx"
elementFormDefault="qualified"
targetNamespace="http://cyclonedx.org/schema/bom/1.1"
vc:minVersion="1.0"
vc:maxVersion="1.1"
version="1.1">
<xs:import namespace="http://cyclonedx.org/schema/spdx" schemaLocation="spdx.SNAPSHOT.xsd"/>
<xs:annotation>
<xs:documentation>
<name>CycloneDX Software Bill-of-Material Specification</name>
<url>https://cyclonedx.org/</url>
<license uri="http://www.apache.org/licenses/LICENSE-2.0"
version="2.0">Apache License, Version 2.0</license>
<authors>
<author>Steve Springett</author>
</authors>
</xs:documentation>
</xs:annotation>
<xs:simpleType name="refType">
<xs:annotation>
<xs:documentation>Identifier-DataType for interlinked elements.</xs:documentation>
</xs:annotation>
<xs:restriction base="xs:string" />
</xs:simpleType>
<xs:complexType name="componentsType">
<xs:sequence minOccurs="0" maxOccurs="unbounded">
<xs:element name="component" type="bom:component"/>
<xs:any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded">
<xs:annotation>
<xs:documentation>
Allows any undeclared elements as long as the elements are placed in a different namespace.
</xs:documentation>
</xs:annotation>
</xs:any>
</xs:sequence>
<xs:anyAttribute namespace="##any" processContents="lax">
<xs:annotation>
<xs:documentation>User-defined attributes may be used on this element as long as they
do not have the same name as an existing attribute used by the schema.</xs:documentation>
</xs:annotation>
</xs:anyAttribute>
</xs:complexType>
<xs:complexType name="component">
<xs:sequence>
<xs:element name="publisher" type="xs:normalizedString" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation>The person(s) or organization(s) that published the component</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="group" type="xs:normalizedString" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation>The grouping name or identifier. This will often be a shortened, single
name of the company or project that produced the component, or the source package or
domain name. Whitespace and special characters should be avoided. Examples include:
apache, org.apache.commons, and apache.org.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="name" type="xs:normalizedString" minOccurs="1" maxOccurs="1">
<xs:annotation>
<xs:documentation>The name of the component. This will often be a shortened, single name
of the component. Examples: commons-lang3 and jquery</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="version" type="xs:normalizedString" minOccurs="1" maxOccurs="1">
<xs:annotation>
<xs:documentation>The component version. The version should ideally comply with semantic versioning
but is not enforced.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="description" type="xs:normalizedString" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation>Specifies a description for the component</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="scope" type="bom:scope" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation>Specifies the scope of the component. If scope is not specified, 'runtime'
scope should be assumed by the consumer of the BOM</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="hashes" minOccurs="0" maxOccurs="1">
<xs:complexType>
<xs:sequence minOccurs="0" maxOccurs="unbounded">
<xs:element name="hash" type="bom:hashType"/>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="licenses" minOccurs="0" maxOccurs="1">
<xs:complexType>
<xs:choice>
<xs:element name="license" type="bom:licenseType" minOccurs="0" maxOccurs="unbounded"/>
<xs:element name="expression" type="xs:normalizedString" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation>A valid SPDX license expression.
Refer to https://spdx.org/specifications for syntax requirements</xs:documentation>
</xs:annotation>
</xs:element>
</xs:choice>
</xs:complexType>
</xs:element>
<xs:element name="copyright" type="xs:normalizedString" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation>An optional copyright notice informing users of the underlying claims to
copyright ownership in a published work.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="cpe" type="bom:cpe" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation>
DEPRECATED - DO NOT USE. This will be removed in a future version.
Specifies a well-formed CPE name. See https://nvd.nist.gov/products/cpe
</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="purl" type="xs:anyURI" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation>
Specifies the package-url (PURL). The purl, if specified, must be valid and conform
to the specification defined at: https://github.com/package-url/purl-spec
</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="modified" type="xs:boolean" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation>
DEPRECATED - DO NOT USE. This will be removed in a future version. Use the pedigree
element instead to supply information on exactly how the component was modified.
A boolean value indicating is the component has been modified from the original.
A value of true indicates the component is a derivative of the original.
A value of false indicates the component has not been modified from the original.
</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="pedigree" type="bom:pedigreeType" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation>
Component pedigree is a way to document complex supply chain scenarios where components are
created, distributed, modified, redistributed, combined with other components, etc.
</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="externalReferences" type="bom:externalReferences" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation>Provides the ability to document external references related to the
component or to the project the component describes.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="components" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation>
Specifies optional sub-components. This is not a dependency tree. It provides a way
to specify a hierarchical representation of component assemblies, similar to
system -> subsystem -> parts assembly in physical supply chains.
</xs:documentation>
</xs:annotation>
<xs:complexType>
<xs:sequence minOccurs="0" maxOccurs="unbounded">
<xs:element name="component" type="bom:component"/>
<xs:any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded">
<xs:annotation>
<xs:documentation>
Allows any undeclared elements as long as the elements are placed in a different namespace.
</xs:documentation>
</xs:annotation>
</xs:any>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded">
<xs:annotation>
<xs:documentation>
Allows any undeclared elements as long as the elements are placed in a different namespace.
</xs:documentation>
</xs:annotation>
</xs:any>
</xs:sequence>
<xs:attribute name="type" type="bom:classification" use="required">
<xs:annotation>
<xs:documentation>
Specifies the type of component. For software components, classify as application if no more
specific appropriate classification is available or cannot be determined for the component.
Valid choices are: application, framework, library, operating-system, device, or file
Refer to the bom:classification documentation for information describing each one
</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="bom-ref" type="bom:refType">
<xs:annotation>
<xs:documentation>
An optional identifier which can be used to reference the component elsewhere in the BOM.
Uniqueness is enforced within all elements and children of the root-level bom element.
</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:anyAttribute namespace="##any" processContents="lax">
<xs:annotation>
<xs:documentation>User-defined attributes may be used on this element as long as they
do not have the same name as an existing attribute used by the schema.</xs:documentation>
</xs:annotation>
</xs:anyAttribute>
</xs:complexType>
<xs:complexType name="licenseType">
<xs:sequence>
<xs:choice>
<xs:element name="id" type="spdx:licenseId" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation>A valid SPDX license ID</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="name" type="xs:normalizedString" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation>If SPDX does not define the license used, this field may be used to provide the license name</xs:documentation>
</xs:annotation>
</xs:element>
</xs:choice>
<xs:element name="text" type="bom:licenseTextType" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation>Specifies the optional full text of the license</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="url" type="xs:anyURI" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation>The URL to the license file. If specified, a 'license'
externalReference should also be specified for completeness.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded">
<xs:annotation>
<xs:documentation>
Allows any undeclared elements as long as the elements are placed in a different namespace.
</xs:documentation>
</xs:annotation>
</xs:any>
</xs:sequence>
</xs:complexType>
<xs:complexType name="licenseTextType">
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:annotation>
<xs:documentation>Specifies attributes of the license text</xs:documentation>
</xs:annotation>
<xs:attribute name="content-type" type="xs:normalizedString" default="text/plain">
<xs:annotation>
<xs:documentation>Specifies the content type of the license text. Defaults to text/plain
if not specified.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="encoding" type="bom:encoding">
<xs:annotation>
<xs:documentation>
Specifies the optional encoding the license text is represented in
</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
<xs:complexType name="hashType">
<xs:annotation>
<xs:documentation>Specifies the file hash of the component</xs:documentation>
</xs:annotation>
<xs:simpleContent>
<xs:extension base="bom:hashValue">
<xs:attribute name="alg" type="bom:hashAlg" use="required">
<xs:annotation>
<xs:documentation>Specifies the algorithm used to create the hash</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
<xs:simpleType name="scope">
<xs:restriction base="xs:string">
<xs:enumeration value="required">
<xs:annotation>
<xs:documentation>The component is required for runtime</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="optional">
<xs:annotation>
<xs:documentation>The component is optional at runtime. Optional components are components that
are not capable of being called due to them not be installed or otherwise accessible by any means.
Components that are installed but due to configuration or other restrictions are prohibited from
being called must be scoped as 'required'.</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="excluded">
<xs:annotation>
<xs:documentation>Components that are excluded provide the ability to document component usage
for test and other non-runtime purposes. Excluded components are not reachable within a call
graph at runtime.</xs:documentation>
</xs:annotation>
</xs:enumeration>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="classification">
<xs:restriction base="xs:string">
<xs:enumeration value="application">
<xs:annotation>
<xs:documentation>A software application. Refer to https://en.wikipedia.org/wiki/Application_software
for information about applications.</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="framework">
<xs:annotation>
<xs:documentation>A software framework. Refer to https://en.wikipedia.org/wiki/Software_framework
for information on how frameworks vary slightly from libraries.</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="library">
<xs:annotation>
<xs:documentation>A software library. Refer to https://en.wikipedia.org/wiki/Library_(computing)
for information about libraries. All third-party and open source reusable components will likely
be a library. If the library also has key features of a framework, then it should be classified
as a framework. If not, or is unknown, then specifying library is recommended.</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="operating-system">
<xs:annotation>
<xs:documentation>A software operating system without regard to deployment model
(i.e. installed on physical hardware, virtual machine, container image, etc) Refer to
https://en.wikipedia.org/wiki/Operating_system</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="device">
<xs:annotation>
<xs:documentation>A hardware device such as a processor, or chip-set. A hardware device
containing firmware should include a component for the physical hardware itself, and another
component of type 'application' or 'operating-system' (whichever is relevant), describing
information about the firmware.</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="file">
<xs:annotation>
<xs:documentation>A computer file. Refer to https://en.wikipedia.org/wiki/Computer_file
for information about files.</xs:documentation>
</xs:annotation>
</xs:enumeration>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="hashAlg">
<xs:restriction base="xs:string">
<xs:enumeration value="MD5"/>
<xs:enumeration value="SHA-1"/>
<xs:enumeration value="SHA-256"/>
<xs:enumeration value="SHA-384"/>
<xs:enumeration value="SHA-512"/>
<xs:enumeration value="SHA3-256"/>
<xs:enumeration value="SHA3-512"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="hashValue">
<xs:restriction base="xs:token">
<xs:pattern value="([a-fA-F0-9]{32})|([a-fA-F0-9]{40})|([a-fA-F0-9]{64})|([a-fA-F0-9]{96})|([a-fA-F0-9]{128})"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="encoding">
<xs:restriction base="xs:string">
<xs:enumeration value="base64"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="cpe">
<xs:annotation>
<xs:documentation xml:lang="en">
Define the format for acceptable CPE URIs. Supports CPE 2.2 and CPE 2.3 formats.
Refer to https://nvd.nist.gov/products/cpe for official specification.
</xs:documentation>
</xs:annotation>
<xs:restriction base="xs:string">
<xs:pattern value="([c][pP][eE]:/[AHOaho]?(:[A-Za-z0-9\._\-~%]*){0,6})|(cpe:2\.3:[aho\*\-](:(((\?*|\*?)([a-zA-Z0-9\-\._]|(\\[\\\*\?!&quot;#$$%&amp;'\(\)\+,/:;&lt;=&gt;@\[\]\^`\{\|}~]))+(\?*|\*?))|[\*\-])){5}(:(([a-zA-Z]{2,3}(-([a-zA-Z]{2}|[0-9]{3}))?)|[\*\-]))(:(((\?*|\*?)([a-zA-Z0-9\-\._]|(\\[\\\*\?!&quot;#$$%&amp;'\(\)\+,/:;&lt;=&gt;@\[\]\^`\{\|}~]))+(\?*|\*?))|[\*\-])){4})"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="urnUuid">
<xs:annotation>
<xs:documentation xml:lang="en">
Defines a string representation of a UUID conforming to RFC 4122.
</xs:documentation>
</xs:annotation>
<xs:restriction base="xs:string">
<xs:pattern value="urn:uuid:([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})|(\{[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\})"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="externalReferenceType">
<xs:restriction base="xs:string">
<xs:enumeration value="vcs">
<xs:annotation>
<xs:documentation>Version Control System</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="issue-tracker">
<xs:annotation>
<xs:documentation>Issue or defect tracking system, or an Application Lifecycle Management (ALM) system</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="website">
<xs:annotation>
<xs:documentation>Website</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="advisories">
<xs:annotation>
<xs:documentation>Security advisories</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="bom">
<xs:annotation>
<xs:documentation>Bill-of-material document (CycloneDX, SPDX, SWID, etc)</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="mailing-list">
<xs:annotation>
<xs:documentation>Mailing list or discussion group</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="social">
<xs:annotation>
<xs:documentation>Social media account</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="chat">
<xs:annotation>
<xs:documentation>Real-time chat platform</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="documentation">
<xs:annotation>
<xs:documentation>Documentation, guides, or how-to instructions</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="support">
<xs:annotation>
<xs:documentation>Community or commercial support</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="distribution">
<xs:annotation>
<xs:documentation>Direct or repository download location</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="license">
<xs:annotation>
<xs:documentation>The URL to the license file. If a license URL has been defined in the license
node, it should also be defined as an external reference for completeness</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="build-meta">
<xs:annotation>
<xs:documentation>Build-system specific meta file (i.e. pom.xml, package.json, .nuspec, etc)</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="build-system">
<xs:annotation>
<xs:documentation>URL to an automated build system</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="other">
<xs:annotation>
<xs:documentation>Use this if no other types accurately describe the purpose of the external reference</xs:documentation>
</xs:annotation>
</xs:enumeration>
</xs:restriction>
</xs:simpleType>
<xs:complexType name="externalReferences">
<xs:annotation>
<xs:documentation xml:lang="en">
External references provide a way to document systems, sites, and information that may be relevant
but which are not included with the BOM.
</xs:documentation>
</xs:annotation>
<xs:sequence minOccurs="0" maxOccurs="unbounded">
<xs:element name="reference" type="bom:externalReference">
<xs:annotation>
<xs:documentation xml:lang="en">Zero or more external references can be defined</xs:documentation>
</xs:annotation>
</xs:element>
</xs:sequence>
</xs:complexType>
<xs:complexType name="externalReference">
<xs:sequence>
<xs:element name="url" type="xs:anyURI" minOccurs="1" maxOccurs="1">
<xs:annotation>
<xs:documentation xml:lang="en">The URL to the external reference</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="comment" type="xs:string" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation xml:lang="en">An optional comment describing the external reference</xs:documentation>
</xs:annotation>
</xs:element>
</xs:sequence>
<xs:attribute name="type" type="bom:externalReferenceType" use="required">
<xs:annotation>
<xs:documentation>Specifies the type of external reference. There are built-in types to describe common
references. If a type does not exist for the reference being referred to, use the "other" type.
</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:anyAttribute namespace="##any" processContents="lax">
<xs:annotation>
<xs:documentation>User-defined attributes may be used on this element as long as they
do not have the same name as an existing attribute used by the schema.</xs:documentation>
</xs:annotation>
</xs:anyAttribute>
</xs:complexType>
<xs:complexType name="commitsType">
<xs:annotation>
<xs:documentation xml:lang="en">Zero or more commits can be specified.</xs:documentation>
</xs:annotation>
<xs:sequence minOccurs="0" maxOccurs="unbounded">
<xs:element name="commit" type="bom:commitType">
<xs:annotation>
<xs:documentation xml:lang="en">Specifies an individual commit.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded">
<xs:annotation>
<xs:documentation>
Allows any undeclared elements as long as the elements are placed in a different namespace.
</xs:documentation>
</xs:annotation>
</xs:any>
</xs:sequence>
</xs:complexType>
<xs:complexType name="commitType">
<xs:sequence>
<xs:element name="uid" type="xs:normalizedString" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation xml:lang="en">A unique identifier of the commit. This may be version control
specific. For example, Subversion uses revision numbers whereas git uses commit hashes.
</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="url" type="xs:anyURI" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation xml:lang="en">The URL to the commit. This URL will typically point to a commit
in a version control system.
</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="author" type="bom:identifiableActionType" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation xml:lang="en">The author who created the changes in the commit</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="committer" type="bom:identifiableActionType" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation xml:lang="en">The person who committed or pushed the commit</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="message" type="xs:normalizedString" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation xml:lang="en">The text description of the contents of the commit</xs:documentation>
</xs:annotation>
</xs:element>
<xs:any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded">
<xs:annotation>
<xs:documentation>
Allows any undeclared elements as long as the elements are placed in a different namespace.
</xs:documentation>
</xs:annotation>
</xs:any>
</xs:sequence>
</xs:complexType>
<xs:complexType name="identifiableActionType">
<xs:sequence>
<xs:element name="timestamp" type="xs:dateTime" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation xml:lang="en">The timestamp in which the action occurred</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="name" type="xs:normalizedString" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation xml:lang="en">The name of the individual who performed the action</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="email" type="xs:normalizedString" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation xml:lang="en">The email address of the individual who performed the action</xs:documentation>
</xs:annotation>
</xs:element>
<xs:any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded">
<xs:annotation>
<xs:documentation>
Allows any undeclared elements as long as the elements are placed in a different namespace.
</xs:documentation>
</xs:annotation>
</xs:any>
</xs:sequence>
</xs:complexType>
<xs:complexType name="pedigreeType">
<xs:annotation>
<xs:documentation xml:lang="en">
Component pedigree is a way to document complex supply chain scenarios where components are created,
distributed, modified, redistributed, combined with other components, etc. Pedigree supports viewing
this complex chain from the beginning, the end, or anywhere in the middle. It also provides a way to
document variants where the exact relation may not be known.
</xs:documentation>
</xs:annotation>
<xs:sequence>
<xs:element name="ancestors" type="bom:componentsType" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation xml:lang="en">Describes zero or more components in which a component is derived
from. This is commonly used to describe forks from existing projects where the forked version
contains a ancestor node containing the original component it was forked from. For example,
Component A is the original component. Component B is the component being used and documented
in the BOM. However, Component B contains a pedigree node with a single ancestor documenting
Component A - the original component from which Component B is derived from.
</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="descendants" type="bom:componentsType" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation xml:lang="en">Descendants are the exact opposite of ancestors. This provides a
way to document all forks (and their forks) of an original or root component.
</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="variants" type="bom:componentsType" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation xml:lang="en">Variants describe relations where the relationship between the
components are not known. For example, if Component A contains nearly identical code to
Component B. They are both related, but it is unclear if one is derived from the other,
or if they share a common ancestor.
</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="commits" type="bom:commitsType" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation xml:lang="en">A list of zero or more commits which provide a trail describing
how the component deviates from an ancestor, descendant, or variant.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="notes" type="xs:string" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation xml:lang="en">Notes, observations, and other non-structured commentary
describing the components pedigree.
</xs:documentation>
</xs:annotation>
</xs:element>
<xs:any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded">
<xs:annotation>
<xs:documentation>
Allows any undeclared elements as long as the elements are placed in a different namespace.
</xs:documentation>
</xs:annotation>
</xs:any>
</xs:sequence>
</xs:complexType>
<xs:element name="bom">
<xs:complexType>
<xs:sequence>
<xs:element name="components" type="bom:componentsType"/>
<xs:element name="externalReferences" type="bom:externalReferences" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation>Provides the ability to document external references related to the BOM or
to the project the BOM describes.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded">
<xs:annotation>
<xs:documentation>
Allows any undeclared elements as long as the elements are placed in a different namespace.
</xs:documentation>
</xs:annotation>
</xs:any>
</xs:sequence>
<xs:attribute name="version" type="xs:integer" default="1">
<xs:annotation>
<xs:documentation>The version allows component publishers/authors to make changes to existing
BOMs to update various aspects of the document such as description or licenses. When a system
is presented with multiple BOMs for the same component, the system should use the most recent
version of the BOM. The default version is '1' and should be incremented for each version of the
BOM that is published. Each version of a component should have a unique BOM and if no changes are
made to the BOMs, then each BOM will have a version of '1'.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="serialNumber" type="bom:urnUuid">
<xs:annotation>
<xs:documentation>Every BOM generated should have a unique serial number, even if the contents
of the BOM being generated have not changed over time. The process or tool responsible for
creating the BOM should create random UUID's for every BOM generated.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:anyAttribute namespace="##any" processContents="lax">
<xs:annotation>
<xs:documentation>User-defined attributes may be used on this element as long as they
do not have the same name as an existing attribute used by the schema.</xs:documentation>
</xs:annotation>
</xs:anyAttribute>
</xs:complexType>
<xs:unique name="bom-ref">
<xs:selector xpath=".//*"/>
<xs:field xpath="@bom-ref"/>
</xs:unique>
</xs:element>
</xs:schema>

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,240 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "http://cyclonedx.org/schema/jsf-0.82.schema.json",
"type": "object",
"title": "JSON Signature Format (JSF) standard",
"$comment" : "JSON Signature Format schema is published under the terms of the Apache License 2.0. JSF was developed by Anders Rundgren (anders.rundgren.net@gmail.com) as a part of the OpenKeyStore project. This schema supports the entirely of the JSF standard excluding 'extensions'.",
"definitions": {
"signature": {
"type": "object",
"title": "Signature",
"oneOf": [
{
"additionalProperties": false,
"properties": {
"signers": {
"type": "array",
"title": "Signature",
"description": "Unique top level property for Multiple Signatures. (multisignature)",
"items": {"$ref": "#/definitions/signer"}
}
}
},
{
"additionalProperties": false,
"properties": {
"chain": {
"type": "array",
"title": "Signature",
"description": "Unique top level property for Signature Chains. (signaturechain)",
"items": {"$ref": "#/definitions/signer"}
}
}
},
{
"title": "Signature",
"description": "Unique top level property for simple signatures. (signaturecore)",
"$ref": "#/definitions/signer"
}
]
},
"signer": {
"type": "object",
"title": "Signature",
"required": [
"algorithm",
"value"
],
"additionalProperties": false,
"properties": {
"algorithm": {
"oneOf": [
{
"type": "string",
"title": "Algorithm",
"description": "Signature algorithm. The currently recognized JWA [RFC7518] and RFC8037 [RFC8037] asymmetric key algorithms. Note: Unlike RFC8037 [RFC8037] JSF requires explicit Ed* algorithm names instead of \"EdDSA\".",
"enum": [
"RS256",
"RS384",
"RS512",
"PS256",
"PS384",
"PS512",
"ES256",
"ES384",
"ES512",
"Ed25519",
"Ed448",
"HS256",
"HS384",
"HS512"
]
},
{
"type": "string",
"title": "Algorithm",
"description": "Signature algorithm. Note: If proprietary signature algorithms are added, they must be expressed as URIs.",
"format": "uri"
}
]
},
"keyId": {
"type": "string",
"title": "Key ID",
"description": "Optional. Application specific string identifying the signature key."
},
"publicKey": {
"title": "Public key",
"description": "Optional. Public key object.",
"$ref": "#/definitions/publicKey"
},
"certificatePath": {
"type": "array",
"title": "Certificate path",
"description": "Optional. Sorted array of X.509 [RFC5280] certificates, where the first element must contain the signature certificate. The certificate path must be contiguous but is not required to be complete.",
"items": {
"type": "string"
}
},
"excludes": {
"type": "array",
"title": "Excludes",
"description": "Optional. Array holding the names of one or more application level properties that must be excluded from the signature process. Note that the \"excludes\" property itself, must also be excluded from the signature process. Since both the \"excludes\" property and the associated data it points to are unsigned, a conforming JSF implementation must provide options for specifying which properties to accept.",
"items": {
"type": "string"
}
},
"value": {
"type": "string",
"title": "Signature",
"description": "The signature data. Note that the binary representation must follow the JWA [RFC7518] specifications."
}
}
},
"keyType": {
"type": "string",
"title": "Key type",
"description": "Key type indicator.",
"enum": [
"EC",
"OKP",
"RSA"
]
},
"publicKey": {
"title": "Public key",
"description": "Optional. Public key object.",
"type": "object",
"required": [
"kty"
],
"additionalProperties": true,
"properties": {
"kty": {
"$ref": "#/definitions/keyType"
}
},
"allOf": [
{
"if": {
"properties": { "kty": { "const": "EC" } }
},
"then": {
"required": [
"kty",
"crv",
"x",
"y"
],
"additionalProperties": false,
"properties": {
"kty": {
"$ref": "#/definitions/keyType"
},
"crv": {
"type": "string",
"title": "Curve name",
"description": "EC curve name.",
"enum": [
"P-256",
"P-384",
"P-521"
]
},
"x": {
"type": "string",
"title": "Coordinate",
"description": "EC curve point X. The length of this field must be the full size of a coordinate for the curve specified in the \"crv\" parameter. For example, if the value of \"crv\" is \"P-521\", the decoded argument must be 66 bytes."
},
"y": {
"type": "string",
"title": "Coordinate",
"description": "EC curve point Y. The length of this field must be the full size of a coordinate for the curve specified in the \"crv\" parameter. For example, if the value of \"crv\" is \"P-256\", the decoded argument must be 32 bytes."
}
}
}
},
{
"if": {
"properties": { "kty": { "const": "OKP" } }
},
"then": {
"required": [
"kty",
"crv",
"x"
],
"additionalProperties": false,
"properties": {
"kty": {
"$ref": "#/definitions/keyType"
},
"crv": {
"type": "string",
"title": "Curve name",
"description": "EdDSA curve name.",
"enum": [
"Ed25519",
"Ed448"
]
},
"x": {
"type": "string",
"title": "Coordinate",
"description": "EdDSA curve point X. The length of this field must be the full size of a coordinate for the curve specified in the \"crv\" parameter. For example, if the value of \"crv\" is \"Ed25519\", the decoded argument must be 32 bytes."
}
}
}
},
{
"if": {
"properties": { "kty": { "const": "RSA" } }
},
"then": {
"required": [
"kty",
"n",
"e"
],
"additionalProperties": false,
"properties": {
"kty": {
"$ref": "#/definitions/keyType"
},
"n": {
"type": "string",
"title": "Modulus",
"description": "RSA modulus."
},
"e": {
"type": "string",
"title": "Exponent",
"description": "RSA exponent."
}
}
}
}
]
}
}
}

View File

@@ -0,0 +1,737 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "http://cyclonedx.org/schema/spdx.schema.json",
"$comment": "v1.0-3.24.0",
"type": "string",
"enum": [
"0BSD",
"3D-Slicer-1.0",
"AAL",
"Abstyles",
"AdaCore-doc",
"Adobe-2006",
"Adobe-Display-PostScript",
"Adobe-Glyph",
"Adobe-Utopia",
"ADSL",
"AFL-1.1",
"AFL-1.2",
"AFL-2.0",
"AFL-2.1",
"AFL-3.0",
"Afmparse",
"AGPL-1.0",
"AGPL-1.0-only",
"AGPL-1.0-or-later",
"AGPL-3.0",
"AGPL-3.0-only",
"AGPL-3.0-or-later",
"Aladdin",
"AMD-newlib",
"AMDPLPA",
"AML",
"AML-glslang",
"AMPAS",
"ANTLR-PD",
"ANTLR-PD-fallback",
"any-OSI",
"Apache-1.0",
"Apache-1.1",
"Apache-2.0",
"APAFML",
"APL-1.0",
"App-s2p",
"APSL-1.0",
"APSL-1.1",
"APSL-1.2",
"APSL-2.0",
"Arphic-1999",
"Artistic-1.0",
"Artistic-1.0-cl8",
"Artistic-1.0-Perl",
"Artistic-2.0",
"ASWF-Digital-Assets-1.0",
"ASWF-Digital-Assets-1.1",
"Baekmuk",
"Bahyph",
"Barr",
"bcrypt-Solar-Designer",
"Beerware",
"Bitstream-Charter",
"Bitstream-Vera",
"BitTorrent-1.0",
"BitTorrent-1.1",
"blessing",
"BlueOak-1.0.0",
"Boehm-GC",
"Borceux",
"Brian-Gladman-2-Clause",
"Brian-Gladman-3-Clause",
"BSD-1-Clause",
"BSD-2-Clause",
"BSD-2-Clause-Darwin",
"BSD-2-Clause-first-lines",
"BSD-2-Clause-FreeBSD",
"BSD-2-Clause-NetBSD",
"BSD-2-Clause-Patent",
"BSD-2-Clause-Views",
"BSD-3-Clause",
"BSD-3-Clause-acpica",
"BSD-3-Clause-Attribution",
"BSD-3-Clause-Clear",
"BSD-3-Clause-flex",
"BSD-3-Clause-HP",
"BSD-3-Clause-LBNL",
"BSD-3-Clause-Modification",
"BSD-3-Clause-No-Military-License",
"BSD-3-Clause-No-Nuclear-License",
"BSD-3-Clause-No-Nuclear-License-2014",
"BSD-3-Clause-No-Nuclear-Warranty",
"BSD-3-Clause-Open-MPI",
"BSD-3-Clause-Sun",
"BSD-4-Clause",
"BSD-4-Clause-Shortened",
"BSD-4-Clause-UC",
"BSD-4.3RENO",
"BSD-4.3TAHOE",
"BSD-Advertising-Acknowledgement",
"BSD-Attribution-HPND-disclaimer",
"BSD-Inferno-Nettverk",
"BSD-Protection",
"BSD-Source-beginning-file",
"BSD-Source-Code",
"BSD-Systemics",
"BSD-Systemics-W3Works",
"BSL-1.0",
"BUSL-1.1",
"bzip2-1.0.5",
"bzip2-1.0.6",
"C-UDA-1.0",
"CAL-1.0",
"CAL-1.0-Combined-Work-Exception",
"Caldera",
"Caldera-no-preamble",
"Catharon",
"CATOSL-1.1",
"CC-BY-1.0",
"CC-BY-2.0",
"CC-BY-2.5",
"CC-BY-2.5-AU",
"CC-BY-3.0",
"CC-BY-3.0-AT",
"CC-BY-3.0-AU",
"CC-BY-3.0-DE",
"CC-BY-3.0-IGO",
"CC-BY-3.0-NL",
"CC-BY-3.0-US",
"CC-BY-4.0",
"CC-BY-NC-1.0",
"CC-BY-NC-2.0",
"CC-BY-NC-2.5",
"CC-BY-NC-3.0",
"CC-BY-NC-3.0-DE",
"CC-BY-NC-4.0",
"CC-BY-NC-ND-1.0",
"CC-BY-NC-ND-2.0",
"CC-BY-NC-ND-2.5",
"CC-BY-NC-ND-3.0",
"CC-BY-NC-ND-3.0-DE",
"CC-BY-NC-ND-3.0-IGO",
"CC-BY-NC-ND-4.0",
"CC-BY-NC-SA-1.0",
"CC-BY-NC-SA-2.0",
"CC-BY-NC-SA-2.0-DE",
"CC-BY-NC-SA-2.0-FR",
"CC-BY-NC-SA-2.0-UK",
"CC-BY-NC-SA-2.5",
"CC-BY-NC-SA-3.0",
"CC-BY-NC-SA-3.0-DE",
"CC-BY-NC-SA-3.0-IGO",
"CC-BY-NC-SA-4.0",
"CC-BY-ND-1.0",
"CC-BY-ND-2.0",
"CC-BY-ND-2.5",
"CC-BY-ND-3.0",
"CC-BY-ND-3.0-DE",
"CC-BY-ND-4.0",
"CC-BY-SA-1.0",
"CC-BY-SA-2.0",
"CC-BY-SA-2.0-UK",
"CC-BY-SA-2.1-JP",
"CC-BY-SA-2.5",
"CC-BY-SA-3.0",
"CC-BY-SA-3.0-AT",
"CC-BY-SA-3.0-DE",
"CC-BY-SA-3.0-IGO",
"CC-BY-SA-4.0",
"CC-PDDC",
"CC0-1.0",
"CDDL-1.0",
"CDDL-1.1",
"CDL-1.0",
"CDLA-Permissive-1.0",
"CDLA-Permissive-2.0",
"CDLA-Sharing-1.0",
"CECILL-1.0",
"CECILL-1.1",
"CECILL-2.0",
"CECILL-2.1",
"CECILL-B",
"CECILL-C",
"CERN-OHL-1.1",
"CERN-OHL-1.2",
"CERN-OHL-P-2.0",
"CERN-OHL-S-2.0",
"CERN-OHL-W-2.0",
"CFITSIO",
"check-cvs",
"checkmk",
"ClArtistic",
"Clips",
"CMU-Mach",
"CMU-Mach-nodoc",
"CNRI-Jython",
"CNRI-Python",
"CNRI-Python-GPL-Compatible",
"COIL-1.0",
"Community-Spec-1.0",
"Condor-1.1",
"copyleft-next-0.3.0",
"copyleft-next-0.3.1",
"Cornell-Lossless-JPEG",
"CPAL-1.0",
"CPL-1.0",
"CPOL-1.02",
"Cronyx",
"Crossword",
"CrystalStacker",
"CUA-OPL-1.0",
"Cube",
"curl",
"cve-tou",
"D-FSL-1.0",
"DEC-3-Clause",
"diffmark",
"DL-DE-BY-2.0",
"DL-DE-ZERO-2.0",
"DOC",
"Dotseqn",
"DRL-1.0",
"DRL-1.1",
"DSDP",
"dtoa",
"dvipdfm",
"ECL-1.0",
"ECL-2.0",
"eCos-2.0",
"EFL-1.0",
"EFL-2.0",
"eGenix",
"Elastic-2.0",
"Entessa",
"EPICS",
"EPL-1.0",
"EPL-2.0",
"ErlPL-1.1",
"etalab-2.0",
"EUDatagrid",
"EUPL-1.0",
"EUPL-1.1",
"EUPL-1.2",
"Eurosym",
"Fair",
"FBM",
"FDK-AAC",
"Ferguson-Twofish",
"Frameworx-1.0",
"FreeBSD-DOC",
"FreeImage",
"FSFAP",
"FSFAP-no-warranty-disclaimer",
"FSFUL",
"FSFULLR",
"FSFULLRWD",
"FTL",
"Furuseth",
"fwlw",
"GCR-docs",
"GD",
"GFDL-1.1",
"GFDL-1.1-invariants-only",
"GFDL-1.1-invariants-or-later",
"GFDL-1.1-no-invariants-only",
"GFDL-1.1-no-invariants-or-later",
"GFDL-1.1-only",
"GFDL-1.1-or-later",
"GFDL-1.2",
"GFDL-1.2-invariants-only",
"GFDL-1.2-invariants-or-later",
"GFDL-1.2-no-invariants-only",
"GFDL-1.2-no-invariants-or-later",
"GFDL-1.2-only",
"GFDL-1.2-or-later",
"GFDL-1.3",
"GFDL-1.3-invariants-only",
"GFDL-1.3-invariants-or-later",
"GFDL-1.3-no-invariants-only",
"GFDL-1.3-no-invariants-or-later",
"GFDL-1.3-only",
"GFDL-1.3-or-later",
"Giftware",
"GL2PS",
"Glide",
"Glulxe",
"GLWTPL",
"gnuplot",
"GPL-1.0",
"GPL-1.0+",
"GPL-1.0-only",
"GPL-1.0-or-later",
"GPL-2.0",
"GPL-2.0+",
"GPL-2.0-only",
"GPL-2.0-or-later",
"GPL-2.0-with-autoconf-exception",
"GPL-2.0-with-bison-exception",
"GPL-2.0-with-classpath-exception",
"GPL-2.0-with-font-exception",
"GPL-2.0-with-GCC-exception",
"GPL-3.0",
"GPL-3.0+",
"GPL-3.0-only",
"GPL-3.0-or-later",
"GPL-3.0-with-autoconf-exception",
"GPL-3.0-with-GCC-exception",
"Graphics-Gems",
"gSOAP-1.3b",
"gtkbook",
"Gutmann",
"HaskellReport",
"hdparm",
"Hippocratic-2.1",
"HP-1986",
"HP-1989",
"HPND",
"HPND-DEC",
"HPND-doc",
"HPND-doc-sell",
"HPND-export-US",
"HPND-export-US-acknowledgement",
"HPND-export-US-modify",
"HPND-export2-US",
"HPND-Fenneberg-Livingston",
"HPND-INRIA-IMAG",
"HPND-Intel",
"HPND-Kevlin-Henney",
"HPND-Markus-Kuhn",
"HPND-merchantability-variant",
"HPND-MIT-disclaimer",
"HPND-Pbmplus",
"HPND-sell-MIT-disclaimer-xserver",
"HPND-sell-regexpr",
"HPND-sell-variant",
"HPND-sell-variant-MIT-disclaimer",
"HPND-sell-variant-MIT-disclaimer-rev",
"HPND-UC",
"HPND-UC-export-US",
"HTMLTIDY",
"IBM-pibs",
"ICU",
"IEC-Code-Components-EULA",
"IJG",
"IJG-short",
"ImageMagick",
"iMatix",
"Imlib2",
"Info-ZIP",
"Inner-Net-2.0",
"Intel",
"Intel-ACPI",
"Interbase-1.0",
"IPA",
"IPL-1.0",
"ISC",
"ISC-Veillard",
"Jam",
"JasPer-2.0",
"JPL-image",
"JPNIC",
"JSON",
"Kastrup",
"Kazlib",
"Knuth-CTAN",
"LAL-1.2",
"LAL-1.3",
"Latex2e",
"Latex2e-translated-notice",
"Leptonica",
"LGPL-2.0",
"LGPL-2.0+",
"LGPL-2.0-only",
"LGPL-2.0-or-later",
"LGPL-2.1",
"LGPL-2.1+",
"LGPL-2.1-only",
"LGPL-2.1-or-later",
"LGPL-3.0",
"LGPL-3.0+",
"LGPL-3.0-only",
"LGPL-3.0-or-later",
"LGPLLR",
"Libpng",
"libpng-2.0",
"libselinux-1.0",
"libtiff",
"libutil-David-Nugent",
"LiLiQ-P-1.1",
"LiLiQ-R-1.1",
"LiLiQ-Rplus-1.1",
"Linux-man-pages-1-para",
"Linux-man-pages-copyleft",
"Linux-man-pages-copyleft-2-para",
"Linux-man-pages-copyleft-var",
"Linux-OpenIB",
"LOOP",
"LPD-document",
"LPL-1.0",
"LPL-1.02",
"LPPL-1.0",
"LPPL-1.1",
"LPPL-1.2",
"LPPL-1.3a",
"LPPL-1.3c",
"lsof",
"Lucida-Bitmap-Fonts",
"LZMA-SDK-9.11-to-9.20",
"LZMA-SDK-9.22",
"Mackerras-3-Clause",
"Mackerras-3-Clause-acknowledgment",
"magaz",
"mailprio",
"MakeIndex",
"Martin-Birgmeier",
"McPhee-slideshow",
"metamail",
"Minpack",
"MirOS",
"MIT",
"MIT-0",
"MIT-advertising",
"MIT-CMU",
"MIT-enna",
"MIT-feh",
"MIT-Festival",
"MIT-Khronos-old",
"MIT-Modern-Variant",
"MIT-open-group",
"MIT-testregex",
"MIT-Wu",
"MITNFA",
"MMIXware",
"Motosoto",
"MPEG-SSG",
"mpi-permissive",
"mpich2",
"MPL-1.0",
"MPL-1.1",
"MPL-2.0",
"MPL-2.0-no-copyleft-exception",
"mplus",
"MS-LPL",
"MS-PL",
"MS-RL",
"MTLL",
"MulanPSL-1.0",
"MulanPSL-2.0",
"Multics",
"Mup",
"NAIST-2003",
"NASA-1.3",
"Naumen",
"NBPL-1.0",
"NCBI-PD",
"NCGL-UK-2.0",
"NCL",
"NCSA",
"Net-SNMP",
"NetCDF",
"Newsletr",
"NGPL",
"NICTA-1.0",
"NIST-PD",
"NIST-PD-fallback",
"NIST-Software",
"NLOD-1.0",
"NLOD-2.0",
"NLPL",
"Nokia",
"NOSL",
"Noweb",
"NPL-1.0",
"NPL-1.1",
"NPOSL-3.0",
"NRL",
"NTP",
"NTP-0",
"Nunit",
"O-UDA-1.0",
"OAR",
"OCCT-PL",
"OCLC-2.0",
"ODbL-1.0",
"ODC-By-1.0",
"OFFIS",
"OFL-1.0",
"OFL-1.0-no-RFN",
"OFL-1.0-RFN",
"OFL-1.1",
"OFL-1.1-no-RFN",
"OFL-1.1-RFN",
"OGC-1.0",
"OGDL-Taiwan-1.0",
"OGL-Canada-2.0",
"OGL-UK-1.0",
"OGL-UK-2.0",
"OGL-UK-3.0",
"OGTSL",
"OLDAP-1.1",
"OLDAP-1.2",
"OLDAP-1.3",
"OLDAP-1.4",
"OLDAP-2.0",
"OLDAP-2.0.1",
"OLDAP-2.1",
"OLDAP-2.2",
"OLDAP-2.2.1",
"OLDAP-2.2.2",
"OLDAP-2.3",
"OLDAP-2.4",
"OLDAP-2.5",
"OLDAP-2.6",
"OLDAP-2.7",
"OLDAP-2.8",
"OLFL-1.3",
"OML",
"OpenPBS-2.3",
"OpenSSL",
"OpenSSL-standalone",
"OpenVision",
"OPL-1.0",
"OPL-UK-3.0",
"OPUBL-1.0",
"OSET-PL-2.1",
"OSL-1.0",
"OSL-1.1",
"OSL-2.0",
"OSL-2.1",
"OSL-3.0",
"PADL",
"Parity-6.0.0",
"Parity-7.0.0",
"PDDL-1.0",
"PHP-3.0",
"PHP-3.01",
"Pixar",
"pkgconf",
"Plexus",
"pnmstitch",
"PolyForm-Noncommercial-1.0.0",
"PolyForm-Small-Business-1.0.0",
"PostgreSQL",
"PPL",
"PSF-2.0",
"psfrag",
"psutils",
"Python-2.0",
"Python-2.0.1",
"python-ldap",
"Qhull",
"QPL-1.0",
"QPL-1.0-INRIA-2004",
"radvd",
"Rdisc",
"RHeCos-1.1",
"RPL-1.1",
"RPL-1.5",
"RPSL-1.0",
"RSA-MD",
"RSCPL",
"Ruby",
"SAX-PD",
"SAX-PD-2.0",
"Saxpath",
"SCEA",
"SchemeReport",
"Sendmail",
"Sendmail-8.23",
"SGI-B-1.0",
"SGI-B-1.1",
"SGI-B-2.0",
"SGI-OpenGL",
"SGP4",
"SHL-0.5",
"SHL-0.51",
"SimPL-2.0",
"SISSL",
"SISSL-1.2",
"SL",
"Sleepycat",
"SMLNJ",
"SMPPL",
"SNIA",
"snprintf",
"softSurfer",
"Soundex",
"Spencer-86",
"Spencer-94",
"Spencer-99",
"SPL-1.0",
"ssh-keyscan",
"SSH-OpenSSH",
"SSH-short",
"SSLeay-standalone",
"SSPL-1.0",
"StandardML-NJ",
"SugarCRM-1.1.3",
"Sun-PPP",
"Sun-PPP-2000",
"SunPro",
"SWL",
"swrule",
"Symlinks",
"TAPR-OHL-1.0",
"TCL",
"TCP-wrappers",
"TermReadKey",
"TGPPL-1.0",
"threeparttable",
"TMate",
"TORQUE-1.1",
"TOSL",
"TPDL",
"TPL-1.0",
"TTWL",
"TTYP0",
"TU-Berlin-1.0",
"TU-Berlin-2.0",
"UCAR",
"UCL-1.0",
"ulem",
"UMich-Merit",
"Unicode-3.0",
"Unicode-DFS-2015",
"Unicode-DFS-2016",
"Unicode-TOU",
"UnixCrypt",
"Unlicense",
"UPL-1.0",
"URT-RLE",
"Vim",
"VOSTROM",
"VSL-1.0",
"W3C",
"W3C-19980720",
"W3C-20150513",
"w3m",
"Watcom-1.0",
"Widget-Workshop",
"Wsuipa",
"WTFPL",
"wxWindows",
"X11",
"X11-distribute-modifications-variant",
"Xdebug-1.03",
"Xerox",
"Xfig",
"XFree86-1.1",
"xinetd",
"xkeyboard-config-Zinoviev",
"xlock",
"Xnet",
"xpp",
"XSkat",
"xzoom",
"YPL-1.0",
"YPL-1.1",
"Zed",
"Zeeff",
"Zend-2.0",
"Zimbra-1.3",
"Zimbra-1.4",
"Zlib",
"zlib-acknowledgement",
"ZPL-1.1",
"ZPL-2.0",
"ZPL-2.1",
"389-exception",
"Asterisk-exception",
"Asterisk-linking-protocols-exception",
"Autoconf-exception-2.0",
"Autoconf-exception-3.0",
"Autoconf-exception-generic",
"Autoconf-exception-generic-3.0",
"Autoconf-exception-macro",
"Bison-exception-1.24",
"Bison-exception-2.2",
"Bootloader-exception",
"Classpath-exception-2.0",
"CLISP-exception-2.0",
"cryptsetup-OpenSSL-exception",
"DigiRule-FOSS-exception",
"eCos-exception-2.0",
"Fawkes-Runtime-exception",
"FLTK-exception",
"fmt-exception",
"Font-exception-2.0",
"freertos-exception-2.0",
"GCC-exception-2.0",
"GCC-exception-2.0-note",
"GCC-exception-3.1",
"Gmsh-exception",
"GNAT-exception",
"GNOME-examples-exception",
"GNU-compiler-exception",
"gnu-javamail-exception",
"GPL-3.0-interface-exception",
"GPL-3.0-linking-exception",
"GPL-3.0-linking-source-exception",
"GPL-CC-1.0",
"GStreamer-exception-2005",
"GStreamer-exception-2008",
"i2p-gpl-java-exception",
"KiCad-libraries-exception",
"LGPL-3.0-linking-exception",
"libpri-OpenH323-exception",
"Libtool-exception",
"Linux-syscall-note",
"LLGPL",
"LLVM-exception",
"LZMA-exception",
"mif-exception",
"Nokia-Qt-exception-1.1",
"OCaml-LGPL-linking-exception",
"OCCT-exception-1.0",
"OpenJDK-assembly-exception-1.0",
"openvpn-openssl-exception",
"PCRE2-exception",
"PS-or-PDF-font-exception-20170817",
"QPL-1.0-INRIA-2004-exception",
"Qt-GPL-exception-1.0",
"Qt-LGPL-exception-1.1",
"Qwt-exception-1.0",
"RRDtool-FLOSS-exception-2.0",
"SANE-exception",
"SHL-2.0",
"SHL-2.1",
"stunnel-exception",
"SWI-exception",
"Swift-exception",
"Texinfo-exception",
"u-boot-exception-2.0",
"UBDL-exception",
"Universal-FOSS-exception-1.0",
"vsftpd-openssl-exception",
"WxWindows-exception-3.1",
"x11vnc-openssl-exception"
]
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,94 @@
# 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 Dict, Literal, Type
from py_serializable import ViewType
from . import SchemaVersion
class BaseSchemaVersion(ABC, ViewType):
@property
@abstractmethod
def schema_version_enum(self) -> SchemaVersion:
... # pragma: no cover
def get_schema_version(self) -> str:
return self.schema_version_enum.to_version()
class SchemaVersion1Dot6(BaseSchemaVersion):
@property
def schema_version_enum(self) -> Literal[SchemaVersion.V1_6]:
return SchemaVersion.V1_6
class SchemaVersion1Dot5(BaseSchemaVersion):
@property
def schema_version_enum(self) -> Literal[SchemaVersion.V1_5]:
return SchemaVersion.V1_5
class SchemaVersion1Dot4(BaseSchemaVersion):
@property
def schema_version_enum(self) -> Literal[SchemaVersion.V1_4]:
return SchemaVersion.V1_4
class SchemaVersion1Dot3(BaseSchemaVersion):
@property
def schema_version_enum(self) -> Literal[SchemaVersion.V1_3]:
return SchemaVersion.V1_3
class SchemaVersion1Dot2(BaseSchemaVersion):
@property
def schema_version_enum(self) -> Literal[SchemaVersion.V1_2]:
return SchemaVersion.V1_2
class SchemaVersion1Dot1(BaseSchemaVersion):
@property
def schema_version_enum(self) -> Literal[SchemaVersion.V1_1]:
return SchemaVersion.V1_1
class SchemaVersion1Dot0(BaseSchemaVersion):
@property
def schema_version_enum(self) -> Literal[SchemaVersion.V1_0]:
return SchemaVersion.V1_0
SCHEMA_VERSIONS: Dict[SchemaVersion, Type[BaseSchemaVersion]] = {
SchemaVersion.V1_6: SchemaVersion1Dot6,
SchemaVersion.V1_5: SchemaVersion1Dot5,
SchemaVersion.V1_4: SchemaVersion1Dot4,
SchemaVersion.V1_3: SchemaVersion1Dot3,
SchemaVersion.V1_2: SchemaVersion1Dot2,
SchemaVersion.V1_1: SchemaVersion1Dot1,
SchemaVersion.V1_0: SchemaVersion1Dot0,
}

View File

@@ -0,0 +1,100 @@
# 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.
"""
Set of helper classes for use with ``serializable`` when conducting (de-)serialization.
"""
from typing import Any, Optional
from uuid import UUID
# See https://github.com/package-url/packageurl-python/issues/65
from packageurl import PackageURL
from py_serializable.helpers import BaseHelper
from ..exception.serialization import CycloneDxDeserializationException, SerializationOfUnexpectedValueException
from ..model.bom_ref import BomRef
from ..model.license import _LicenseRepositorySerializationHelper
class BomRefHelper(BaseHelper):
"""**DEPRECATED** in favour of :class:`BomRef`.
.. deprecated:: 8.6
Use :class:`BomRef` instead.
"""
# TODO: remove, no longer needed
@classmethod
def serialize(cls, o: Any) -> Optional[str]:
return BomRef.serialize(o)
@classmethod
def deserialize(cls, o: Any) -> BomRef:
return BomRef.deserialize(o)
class PackageUrl(BaseHelper):
@classmethod
def serialize(cls, o: Any, ) -> str:
if isinstance(o, PackageURL):
return str(o.to_string())
raise SerializationOfUnexpectedValueException(
f'Attempt to serialize a non-PackageURL: {o!r}')
@classmethod
def deserialize(cls, o: Any) -> PackageURL:
try:
return PackageURL.from_string(purl=str(o))
except ValueError as err:
raise CycloneDxDeserializationException(
f'PURL string supplied does not parse: {o!r}'
) from err
class UrnUuidHelper(BaseHelper):
@classmethod
def serialize(cls, o: Any) -> str:
if isinstance(o, UUID):
return o.urn
raise SerializationOfUnexpectedValueException(
f'Attempt to serialize a non-UUID: {o!r}')
@classmethod
def deserialize(cls, o: Any) -> UUID:
try:
return UUID(str(o))
except ValueError as err:
raise CycloneDxDeserializationException(
f'UUID string supplied does not parse: {o!r}'
) from err
class LicenseRepositoryHelper(_LicenseRepositorySerializationHelper):
"""**DEPRECATED**
.. deprecated:: 8.6
No public API planned for replacing this,
"""
# TODO: remove, no longer needed
pass

View File

@@ -0,0 +1,77 @@
# 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.
__all__ = [
'is_supported_id', 'fixup_id',
'is_expression'
]
from json import load as json_load
from typing import TYPE_CHECKING, Dict, Optional, Set
from license_expression import get_spdx_licensing # type:ignore[import-untyped]
from .schema._res import SPDX_JSON as __SPDX_JSON_SCHEMA
if TYPE_CHECKING: # pragma: no cover
from license_expression import Licensing
# region init
# python's internal module loader will assure that this init-part runs only once.
# !!! this requires to ship the actual schema data with the package.
with open(__SPDX_JSON_SCHEMA) as schema:
__IDS: Set[str] = set(json_load(schema).get('enum', []))
assert len(__IDS) > 0, 'known SPDX-IDs should be non-empty set'
__IDS_LOWER_MAP: Dict[str, str] = dict((id_.lower(), id_) for id_ in __IDS)
__SPDX_EXPRESSION_LICENSING: 'Licensing' = get_spdx_licensing()
# endregion
def is_supported_id(value: str) -> bool:
"""Validate SPDX-ID according to current spec."""
return value in __IDS
def fixup_id(value: str) -> Optional[str]:
"""Fixup SPDX-ID.
:returns: repaired value string, or `None` if fixup was unable to help.
"""
return __IDS_LOWER_MAP.get(value.lower())
def is_expression(value: str) -> bool:
"""Validate SPDX license expression.
.. note::
Utilizes `license-expression library`_ to
validate SPDX compound expression according to `SPDX license expression spec`_.
.. _SPDX license expression spec: https://spdx.github.io/spdx-spec/v3.0.1/annexes/spdx-license-expressions/
.. _license-expression library: https://github.com/nexB/license-expression
"""
try:
res = __SPDX_EXPRESSION_LICENSING.validate(value)
except Exception:
# the throw happens when internals crash due to unexpected input characters.
return False
return 0 == len(res.errors)

View File

@@ -0,0 +1,121 @@
# 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 TYPE_CHECKING, Any, Literal, Optional, Protocol, Union, overload
from ..schema import OutputFormat
if TYPE_CHECKING: # pragma: no cover
from ..schema import SchemaVersion
from .json import JsonValidator
from .xml import XmlValidator
class ValidationError:
"""Validation failed with this specific error.
Use :attr:`~data` to access the content.
"""
data: Any
def __init__(self, data: Any) -> None:
self.data = data
def __repr__(self) -> str:
return repr(self.data)
def __str__(self) -> str:
return str(self.data)
class SchemabasedValidator(Protocol):
"""Schema-based Validator protocol"""
def validate_str(self, data: str) -> Optional[ValidationError]:
"""Validate a string
:param data: the data string to validate
:return: validation error
:retval None: if ``data`` is valid
:retval ValidationError: if ``data`` is invalid
"""
... # pragma: no cover
class BaseSchemabasedValidator(ABC, SchemabasedValidator):
"""Base Schema-based Validator"""
def __init__(self, schema_version: 'SchemaVersion') -> None:
self.__schema_version = schema_version
if not self._schema_file:
raise ValueError(f'Unsupported schema_version: {schema_version!r}')
@property
def schema_version(self) -> 'SchemaVersion':
"""Get the schema version."""
return self.__schema_version
@property
@abstractmethod
def output_format(self) -> OutputFormat:
"""Get the format."""
... # pragma: no cover
@property
@abstractmethod
def _schema_file(self) -> Optional[str]:
"""Get the schema file according to schema version."""
... # pragma: no cover
@overload
def make_schemabased_validator(output_format: Literal[OutputFormat.JSON], schema_version: 'SchemaVersion'
) -> 'JsonValidator':
... # pragma: no cover
@overload
def make_schemabased_validator(output_format: Literal[OutputFormat.XML], schema_version: 'SchemaVersion'
) -> 'XmlValidator':
... # pragma: no cover
@overload
def make_schemabased_validator(output_format: OutputFormat, schema_version: 'SchemaVersion'
) -> Union['JsonValidator', 'XmlValidator']:
... # pragma: no cover
def make_schemabased_validator(output_format: OutputFormat, schema_version: 'SchemaVersion'
) -> 'BaseSchemabasedValidator':
"""Get the default Schema-based Validator for a certain :class:`OutputFormat`.
Raises error when no instance could be made.
"""
if TYPE_CHECKING: # pragma: no cover
from typing import Type
Validator: Type[BaseSchemabasedValidator] # noqa:N806
if OutputFormat.JSON is output_format:
from .json import JsonValidator as Validator
elif OutputFormat.XML is output_format:
from .xml import XmlValidator as Validator
else:
raise ValueError(f'Unexpected output_format: {output_format!r}')
return Validator(schema_version)

Some files were not shown because too many files have changed in this diff Show More