diff --git a/cyclonedx/builder/__init__.py b/cyclonedx/builder/__init__.py index ec68e667..dc45dec3 100644 --- a/cyclonedx/builder/__init__.py +++ b/cyclonedx/builder/__init__.py @@ -17,4 +17,6 @@ """ Builders used in this library. + +.. deprecated:: next """ diff --git a/cyclonedx/builder/this.py b/cyclonedx/builder/this.py index 8f81a8ff..31f7a8ab 100644 --- a/cyclonedx/builder/this.py +++ b/cyclonedx/builder/this.py @@ -15,69 +15,51 @@ # SPDX-License-Identifier: Apache-2.0 # Copyright (c) OWASP Foundation. All Rights Reserved. -"""Representation of this very python library.""" +"""Representation of this very python library. -__all__ = ['this_component', 'this_tool', ] +.. deprecated:: next +""" -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 +__all__ = ['this_component', 'this_tool'] -# !!! keep this file in sync with `pyproject.toml` +import sys +from typing import TYPE_CHECKING +if sys.version_info >= (3, 13): + from warnings import deprecated +else: + from typing_extensions import deprecated -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... - ) +from ..contrib.this.builders import this_component as _this_component, this_tool as _this_tool +# region deprecated re-export -def this_tool() -> Tool: - """Representation of this very python library as a :class:`Tool`.""" - return Tool.from_component(this_component()) +if TYPE_CHECKING: + from ..model.component import Component + from ..model.tool import Tool + + +@deprecated('Deprecated re-export location - see docstring of "this_component" for details.') +def this_component() -> 'Component': + """Deprecated — Alias of :func:`cyclonedx.contrib.this.builders.this_component`. + + .. deprecated:: next + This re-export location is deprecated. + Use ``from cyclonedx.contrib.this.builders import this_component`` instead. + The exported symbol itself is NOT deprecated — only this import path. + """ + return _this_component() + + +@deprecated('Deprecated re-export location - see docstring of "this_tool" for details.') +def this_tool() -> 'Tool': + """Deprecated — Alias of :func:`cyclonedx.contrib.this.builders.this_tool`. + + .. deprecated:: next + This re-export location is deprecated. + Use ``from cyclonedx.contrib.this.builders import this_tool`` instead. + The exported symbol itself is NOT deprecated — only this import path. + """ + return _this_tool() + +# endregion deprecated re-export diff --git a/cyclonedx/contrib/README.md b/cyclonedx/contrib/README.md new file mode 100644 index 00000000..aff5230e --- /dev/null +++ b/cyclonedx/contrib/README.md @@ -0,0 +1,20 @@ +# CycloneDX Contrib Extensions + +This directory contains community-contributed functionality that extends the capabilities of the CycloneDX core library. +Unlike the modules in `../`, these features are not part of the official CycloneDX specification and may vary in stability, scope, or compatibility. + +## Contents +- Utilities, helpers, and experimental features developed by the community +- Optional add-ons that may facilitate or enhance use of the CycloneDX core library +- Code that evolves independently of the CycloneDX specification + +## Notes +- Contrib modules are optional and not required for strict compliance with the CycloneDX standard. +- They may change more frequently than the core and are not guaranteed to follow the same versioning rules. +- Users should evaluate these modules carefully and consult documentation or source comments for details. + +## Contributing +Contributions are welcome. To add an extension: +1. Follow the contribution guidelines in the main repository. +2. Place your code in a clearly named subfolder or file under `contrib/`. +3. Provide documentation and tests to ensure clarity and maintainability. diff --git a/cyclonedx/_internal/hash.py b/cyclonedx/contrib/__init__.py similarity index 57% rename from cyclonedx/_internal/hash.py rename to cyclonedx/contrib/__init__.py index 4fc17f5e..d0babedd 100644 --- a/cyclonedx/_internal/hash.py +++ b/cyclonedx/contrib/__init__.py @@ -17,27 +17,11 @@ """ -!!! ALL SYMBOLS IN HERE ARE INTERNAL. -Everything might change without any notice. +Some features in this library are marked as contrib. +These are community-provided extensions and are not part of the official standard. +They are optional and may evolve independently from the core. """ - -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() +__all__ = [ + # there is no intention to export anything in here. +] diff --git a/cyclonedx/contrib/component/__init__.py b/cyclonedx/contrib/component/__init__.py new file mode 100644 index 00000000..02d2b73f --- /dev/null +++ b/cyclonedx/contrib/component/__init__.py @@ -0,0 +1,18 @@ +# 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. + +"""Component related functionality""" diff --git a/cyclonedx/contrib/component/builders.py b/cyclonedx/contrib/component/builders.py new file mode 100644 index 00000000..633e186b --- /dev/null +++ b/cyclonedx/contrib/component/builders.py @@ -0,0 +1,75 @@ +# 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. + +"""Component related builders""" + +__all__ = ['ComponentBuilder'] + +from hashlib import sha1 +from os.path import exists +from typing import Optional + +from ...model import HashAlgorithm, HashType +from ...model.component import Component, ComponentType + + +class ComponentBuilder: + + def make_for_file(self, absolute_file_path: str, *, + name: Optional[str]) -> Component: + """ + Helper method to create a :class:`cyclonedx.model.component.Component` + that represents the provided local file as a Component. + + Args: + absolute_file_path: + Absolute path to the file you wish to represent + name: + Optionally, if supplied this is the name that will be used for the component. + Defaults to arg ``absolute_file_path``. + + Returns: + `Component` representing the supplied file + """ + if not exists(absolute_file_path): + raise FileExistsError(f'Supplied file path {absolute_file_path!r} does not exist') + + return Component( + type=ComponentType.FILE, + name=name or absolute_file_path, + hashes=[ + HashType(alg=HashAlgorithm.SHA_1, content=self._file_sha1sum(absolute_file_path)) + ] + ) + + @staticmethod + 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() diff --git a/cyclonedx/contrib/hash/__init__.py b/cyclonedx/contrib/hash/__init__.py new file mode 100644 index 00000000..b65a1aff --- /dev/null +++ b/cyclonedx/contrib/hash/__init__.py @@ -0,0 +1,18 @@ +# 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. + +"""Hash related functionality""" diff --git a/cyclonedx/contrib/hash/factories.py b/cyclonedx/contrib/hash/factories.py new file mode 100644 index 00000000..c4b477fd --- /dev/null +++ b/cyclonedx/contrib/hash/factories.py @@ -0,0 +1,129 @@ +# 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. + +"""Hash related factories""" + +__all__ = ['HashTypeFactory'] + +from ...exception.model import UnknownHashTypeException +from ...model import HashAlgorithm, HashType + +_MAP_HASHLIB: dict[str, HashAlgorithm] = { + # from hashlib.algorithms_guaranteed + 'md5': HashAlgorithm.MD5, + 'sha1': HashAlgorithm.SHA_1, + # sha224: + 'sha256': HashAlgorithm.SHA_256, + 'sha384': HashAlgorithm.SHA_384, + 'sha512': HashAlgorithm.SHA_512, + # blake2b: + # blake2s: + # sha3_224: + 'sha3_256': HashAlgorithm.SHA3_256, + 'sha3_384': HashAlgorithm.SHA3_384, + 'sha3_512': HashAlgorithm.SHA3_512, + # shake_128: + # shake_256: +} + + +class HashTypeFactory: + + def from_hashlib_alg(self, hashlib_alg: str, content: str) -> HashType: + """ + Attempts to convert a hashlib-algorithm to our internal model classes. + + Args: + hashlib_alg: + Hash algorithm - like it is used by `hashlib`. + Example: `sha256`. + + content: + Hash value. + + Raises: + `UnknownHashTypeException` if the algorithm of hash cannot be determined. + + Returns: + An instance of `HashType`. + """ + alg = _MAP_HASHLIB.get(hashlib_alg.lower()) + if alg is None: + raise UnknownHashTypeException(f'Unable to determine hash alg for {hashlib_alg!r}') + return HashType(alg=alg, content=content) + + def from_composite_str(self, composite_hash: str) -> HashType: + """ + Attempts to convert a string which includes both the Hash Algorithm and Hash Value and represent using our + internal model classes. + + Args: + composite_hash: + Composite Hash string of the format `HASH_ALGORITHM`:`HASH_VALUE`. + Example: `sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b`. + + Valid case insensitive prefixes are: + `md5`, `sha1`, `sha256`, `sha384`, `sha512`, `blake2b256`, `blake2b384`, `blake2b512`, + `blake2256`, `blake2384`, `blake2512`, `sha3-256`, `sha3-384`, `sha3-512`, + `blake3`. + + Raises: + `UnknownHashTypeException` if the type of hash cannot be determined. + + Returns: + An instance of `HashType`. + """ + parts = composite_hash.split(':') + + algorithm_prefix = parts[0].lower() + if algorithm_prefix == 'md5': + return HashType( + alg=HashAlgorithm.MD5, + content=parts[1].lower() + ) + elif algorithm_prefix[0:4] == 'sha3': + return HashType( + alg=getattr(HashAlgorithm, f'SHA3_{algorithm_prefix[5:]}'), + content=parts[1].lower() + ) + elif algorithm_prefix == 'sha1': + return HashType( + alg=HashAlgorithm.SHA_1, + content=parts[1].lower() + ) + elif algorithm_prefix[0:3] == 'sha': + # This is actually SHA2... + return HashType( + alg=getattr(HashAlgorithm, f'SHA_{algorithm_prefix[3:]}'), + content=parts[1].lower() + ) + elif algorithm_prefix[0:7] == 'blake2b': + return HashType( + alg=getattr(HashAlgorithm, f'BLAKE2B_{algorithm_prefix[7:]}'), + content=parts[1].lower() + ) + elif algorithm_prefix[0:6] == 'blake2': + return HashType( + alg=getattr(HashAlgorithm, f'BLAKE2B_{algorithm_prefix[6:]}'), + content=parts[1].lower() + ) + elif algorithm_prefix[0:6] == 'blake3': + return HashType( + alg=HashAlgorithm.BLAKE3, + content=parts[1].lower() + ) + raise UnknownHashTypeException(f'Unable to determine hash type from {composite_hash!r}') diff --git a/cyclonedx/contrib/license/__init__.py b/cyclonedx/contrib/license/__init__.py new file mode 100644 index 00000000..cd8b075e --- /dev/null +++ b/cyclonedx/contrib/license/__init__.py @@ -0,0 +1,18 @@ +# 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 functionality""" diff --git a/cyclonedx/contrib/license/exceptions.py b/cyclonedx/contrib/license/exceptions.py new file mode 100644 index 00000000..571c7598 --- /dev/null +++ b/cyclonedx/contrib/license/exceptions.py @@ -0,0 +1,61 @@ +# 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 ...exception import CycloneDxException + +__all__ = ['FactoryException', 'LicenseChoiceFactoryException', 'InvalidSpdxLicenseException', + 'LicenseFactoryException', 'InvalidLicenseExpressionException'] + + +class FactoryException(CycloneDxException): + """ + Base exception that covers all exceptions that may be thrown during model factoring. + """ + pass + + +class LicenseChoiceFactoryException(FactoryException): + """ + Base exception that covers all LicenseChoiceFactory exceptions. + """ + pass + + +class InvalidSpdxLicenseException(LicenseChoiceFactoryException): + """ + Thrown when an invalid SPDX License is provided. + """ + pass + + +class LicenseFactoryException(FactoryException): + """ + Base exception that covers all LicenseFactory exceptions. + """ + pass + + +class InvalidLicenseExpressionException(LicenseFactoryException): + """ + Thrown when an invalid License expression is provided. + """ + pass diff --git a/cyclonedx/contrib/license/factories.py b/cyclonedx/contrib/license/factories.py new file mode 100644 index 00000000..581acbed --- /dev/null +++ b/cyclonedx/contrib/license/factories.py @@ -0,0 +1,97 @@ +# 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 factories""" + +__all__ = ['LicenseFactory'] + +from typing import TYPE_CHECKING, Optional + +from ...model.license import DisjunctiveLicense, LicenseExpression +from ...spdx import fixup_id as spdx_fixup, is_expression as is_spdx_expression +from .exceptions import InvalidLicenseExpressionException, InvalidSpdxLicenseException + +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) + + +# Idea for more factories: +# class LicenseAttachmentFactory: +# def make_from_file(self, path: PathLike) -> AttachedText: ... diff --git a/cyclonedx/contrib/this/__init__.py b/cyclonedx/contrib/this/__init__.py new file mode 100644 index 00000000..b6b61320 --- /dev/null +++ b/cyclonedx/contrib/this/__init__.py @@ -0,0 +1,18 @@ +# 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.""" diff --git a/cyclonedx/contrib/this/builders.py b/cyclonedx/contrib/this/builders.py new file mode 100644 index 00000000..2aa5bfe7 --- /dev/null +++ b/cyclonedx/contrib/this/builders.py @@ -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:`cyclonedx.model.component.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:`cyclonedx.model.tool.Tool`.""" + return Tool.from_component(this_component()) diff --git a/cyclonedx/contrib/vulnerability/__init__.py b/cyclonedx/contrib/vulnerability/__init__.py new file mode 100644 index 00000000..5d93d047 --- /dev/null +++ b/cyclonedx/contrib/vulnerability/__init__.py @@ -0,0 +1,18 @@ +# 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. + +"""Vulnerability related functionality""" diff --git a/cyclonedx/contrib/vulnerability/cvss.py b/cyclonedx/contrib/vulnerability/cvss.py new file mode 100644 index 00000000..461b5ee9 --- /dev/null +++ b/cyclonedx/contrib/vulnerability/cvss.py @@ -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. + +"""CVSS related utilities""" + +__all__ = ['vs_from_cvss_scores'] + +from typing import Union + +from ...model.vulnerability import VulnerabilitySeverity + + +def vs_from_cvss_scores(scores: Union[tuple[float, ...], float, None]) -> VulnerabilitySeverity: + """ + Derives the Severity of a Vulnerability from it's declared CVSS scores. + + Args: + scores: A `tuple` of CVSS scores. CVSS scoring system allows for up to three separate scores. + + Returns: + Always returns an instance of :class:`cyclonedx.model.vulnerability.VulnerabilitySeverity`. + """ + if type(scores) is float: + scores = (scores,) + + if scores is None: + return VulnerabilitySeverity.UNKNOWN + + max_cvss_score: float + if isinstance(scores, tuple): + max_cvss_score = max(scores) + else: + max_cvss_score = float(scores) + + if max_cvss_score >= 9.0: + return VulnerabilitySeverity.CRITICAL + elif max_cvss_score >= 7.0: + return VulnerabilitySeverity.HIGH + elif max_cvss_score >= 4.0: + return VulnerabilitySeverity.MEDIUM + elif max_cvss_score > 0.0: + return VulnerabilitySeverity.LOW + else: + return VulnerabilitySeverity.NONE diff --git a/cyclonedx/exception/factory.py b/cyclonedx/exception/factory.py index 2ddbb327..41591e41 100644 --- a/cyclonedx/exception/factory.py +++ b/cyclonedx/exception/factory.py @@ -18,41 +18,69 @@ """ Exceptions relating to specific conditions that occur when factoring a model. + +.. deprecated:: next """ -from . import CycloneDxException +__all__ = ['CycloneDxFactoryException', 'LicenseChoiceFactoryException', + 'InvalidSpdxLicenseException', 'LicenseFactoryException', 'InvalidLicenseExpressionException'] + +from ..contrib.license.exceptions import ( + FactoryException as _FactoryException, + InvalidLicenseExpressionException as _InvalidLicenseExpressionException, + InvalidSpdxLicenseException as _InvalidSpdxLicenseException, + LicenseChoiceFactoryException as _LicenseChoiceFactoryException, + LicenseFactoryException as _LicenseFactoryException, +) + +# region deprecated re-export +# re-export NOT as inherited class with @deprecated, to keep the original subclassing intact!!1 -class CycloneDxFactoryException(CycloneDxException): - """ - Base exception that covers all exceptions that may be thrown during model factoring.. - """ - pass +CycloneDxFactoryException = _FactoryException +"""Deprecated — Alias of :class:`cyclonedx.contrib.license.exceptions.FactoryException`. -class LicenseChoiceFactoryException(CycloneDxFactoryException): - """ - Base exception that covers all LicenseChoiceFactory exceptions. - """ - pass +.. deprecated:: next + This re-export location is deprecated. + Use ``from cyclonedx.contrib.license.exceptions import FactoryException`` instead. + The exported symbol itself is NOT deprecated — only this import path. +""" +LicenseChoiceFactoryException = _LicenseChoiceFactoryException +"""Deprecated — Alias of :class:`cyclonedx.contrib.license.exceptions.LicenseChoiceFactoryException`. -class InvalidSpdxLicenseException(LicenseChoiceFactoryException): - """ - Thrown when an invalid SPDX License is provided. - """ - pass +.. deprecated:: next + This re-export location is deprecated. + Use ``from cyclonedx.contrib.license.exceptions import LicenseChoiceFactoryException`` instead. + The exported symbol itself is NOT deprecated — only this import path. +""" +InvalidSpdxLicenseException = _InvalidSpdxLicenseException +"""Deprecated — Alias of :class:`cyclonedx.contrib.license.exceptions.InvalidSpdxLicenseException`. -class LicenseFactoryException(CycloneDxFactoryException): - """ - Base exception that covers all LicenseFactory exceptions. - """ - pass +.. deprecated:: next + This re-export location is deprecated. + Use ``from cyclonedx.contrib.license.exceptions import InvalidSpdxLicenseException`` instead. + The exported symbol itself is NOT deprecated — only this import path. +""" +LicenseFactoryException = _LicenseFactoryException +"""Deprecated — Alias of :class:`cyclonedx.contrib.license.exceptions.LicenseFactoryException`. + +.. deprecated:: next + This re-export location is deprecated. + Use ``from cyclonedx.contrib.license.exceptions import LicenseFactoryException`` instead. + The exported symbol itself is NOT deprecated — only this import path. +""" + +InvalidLicenseExpressionException = _InvalidLicenseExpressionException +"""Deprecated — Alias of :class:`cyclonedx.contrib.license.exceptions.InvalidLicenseExpressionException`. + +.. deprecated:: next + This re-export location is deprecated. + Use ``from cyclonedx.contrib.license.exceptions import InvalidLicenseExpressionException`` instead. + The exported symbol itself is NOT deprecated — only this import path. +""" -class InvalidLicenseExpressionException(LicenseFactoryException): - """ - Thrown when an invalid License expressions is provided. - """ - pass +# endregion deprecated re-export diff --git a/cyclonedx/exception/model.py b/cyclonedx/exception/model.py index f3986eb9..a38cc386 100644 --- a/cyclonedx/exception/model.py +++ b/cyclonedx/exception/model.py @@ -116,7 +116,7 @@ class UnknownHashTypeException(CycloneDxModelException): """ Exception raised when we are unable to determine the type of hash from a composite hash string. """ - pass + pass # TODO research deprecation of this... class LicenseExpressionAlongWithOthersException(CycloneDxModelException): diff --git a/cyclonedx/factory/__init__.py b/cyclonedx/factory/__init__.py index ffb3ca2f..58f48f6d 100644 --- a/cyclonedx/factory/__init__.py +++ b/cyclonedx/factory/__init__.py @@ -17,4 +17,6 @@ """ Factories used in this library. + +.. deprecated:: next """ diff --git a/cyclonedx/factory/license.py b/cyclonedx/factory/license.py index 40d4484d..043f3967 100644 --- a/cyclonedx/factory/license.py +++ b/cyclonedx/factory/license.py @@ -15,74 +15,33 @@ # SPDX-License-Identifier: Apache-2.0 # Copyright (c) OWASP Foundation. All Rights Reserved. -from typing import TYPE_CHECKING, Optional +""" +.. deprecated:: next +""" -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 +__all__ = ['LicenseFactory'] -if TYPE_CHECKING: # pragma: no cover - from ..model import AttachedText, XsUri - from ..model.license import License, LicenseAcknowledgement +import sys +if sys.version_info >= (3, 13): + from warnings import deprecated +else: + from typing_extensions import deprecated -class LicenseFactory: - """Factory for :class:`cyclonedx.model.license.License`.""" +from ..contrib.license.factories import LicenseFactory as _LicenseFactory - 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) +# region deprecated re-export - 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`. +@deprecated('Deprecated re-export location - see docstring of "LicenseFactory" for details.') +class LicenseFactory(_LicenseFactory): + """Deprecated — Alias of :class:`cyclonedx.contrib.license.factories.LicenseFactory`. - :raises InvalidLicenseExpressionException: if param `value` is not known/supported license expression - """ - if is_spdx_expression(expression): - return LicenseExpression(expression, acknowledgement=acknowledgement) - raise InvalidLicenseExpressionException(expression) + .. deprecated:: next + This re-export location is deprecated. + Use ``from cyclonedx.contrib.license.factories import LicenseFactory`` instead. + The exported symbol itself is NOT deprecated — only this import path. + """ + pass - 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) +# endregion deprecated re-export diff --git a/cyclonedx/model/__init__.py b/cyclonedx/model/__init__.py index ff0abc77..f949e10c 100644 --- a/cyclonedx/model/__init__.py +++ b/cyclonedx/model/__init__.py @@ -23,6 +23,7 @@ """ import re +import sys from collections.abc import Generator, Iterable from datetime import datetime from enum import Enum @@ -34,11 +35,16 @@ from warnings import warn from xml.etree.ElementTree import Element as XmlElement # nosec B405 +if sys.version_info >= (3, 13): + from warnings import deprecated +else: + from typing_extensions import deprecated + import py_serializable as serializable from sortedcontainers import SortedSet from .._internal.compare import ComparableTuple as _ComparableTuple -from ..exception.model import InvalidLocaleTypeException, InvalidUriException, UnknownHashTypeException +from ..exception.model import InvalidLocaleTypeException, InvalidUriException from ..exception.serialization import CycloneDxDeserializationException, SerializationOfUnexpectedValueException from ..schema.schema import ( SchemaVersion1Dot0, @@ -366,25 +372,6 @@ def xml_denormalize(cls, o: 'XmlElement', *, ] -_MAP_HASHLIB: dict[str, HashAlgorithm] = { - # from hashlib.algorithms_guaranteed - 'md5': HashAlgorithm.MD5, - 'sha1': HashAlgorithm.SHA_1, - # sha224: - 'sha256': HashAlgorithm.SHA_256, - 'sha384': HashAlgorithm.SHA_384, - 'sha512': HashAlgorithm.SHA_512, - # blake2b: - # blake2s: - # sha3_224: - 'sha3_256': HashAlgorithm.SHA3_256, - 'sha3_384': HashAlgorithm.SHA3_384, - 'sha3_512': HashAlgorithm.SHA3_512, - # shake_128: - # shake_256: -} - - @serializable.serializable_class class HashType: """ @@ -395,91 +382,33 @@ class HashType: """ @staticmethod + @deprecated('Deprecated - use cyclonedx.contrib.hash.factories.HashTypeFactory().from_hashlib_alg() instead') def from_hashlib_alg(hashlib_alg: str, content: str) -> 'HashType': - """ - Attempts to convert a hashlib-algorithm to our internal model classes. - - Args: - hashlib_alg: - Hash algorith - like it is used by `hashlib`. - Example: `sha256`. + """Deprecated — Alias of :func:`cyclonedx.contrib.hash.factories.HashTypeFactory.from_hashlib_alg`. - content: - Hash value. - - Raises: - `UnknownHashTypeException` if the algorithm of hash cannot be determined. + Attempts to convert a hashlib-algorithm to our internal model classes. - Returns: - An instance of `HashType`. + .. deprecated:: next + Use ``cyclonedx.contrib.hash.factories.HashTypeFactory().from_hashlib_alg()`` instead. """ - alg = _MAP_HASHLIB.get(hashlib_alg.lower()) - if alg is None: - raise UnknownHashTypeException(f'Unable to determine hash alg for {hashlib_alg!r}') - return HashType(alg=alg, content=content) + from ..contrib.hash.factories import HashTypeFactory + + return HashTypeFactory().from_hashlib_alg(hashlib_alg, content) @staticmethod + @deprecated('Deprecated - use cyclonedx.contrib.hash.factories.HashTypeFactory().from_composite_str() instead') def from_composite_str(composite_hash: str) -> 'HashType': - """ + """Deprecated — Alias of :func:`cyclonedx.contrib.hash.factories.HashTypeFactory.from_composite_str`. + Attempts to convert a string which includes both the Hash Algorithm and Hash Value and represent using our internal model classes. - Args: - composite_hash: - Composite Hash string of the format `HASH_ALGORITHM`:`HASH_VALUE`. - Example: `sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b`. - - Valid case insensitive prefixes are: - `md5`, `sha1`, `sha256`, `sha384`, `sha512`, `blake2b256`, `blake2b384`, `blake2b512`, - `blake2256`, `blake2384`, `blake2512`, `sha3-256`, `sha3-384`, `sha3-512`, - `blake3`. - - Raises: - `UnknownHashTypeException` if the type of hash cannot be determined. - - Returns: - An instance of `HashType`. + .. deprecated:: next + Use ``cyclonedx.contrib.hash.factories.HashTypeFactory().from_composite_str()`` instead. """ - parts = composite_hash.split(':') + from ..contrib.hash.factories import HashTypeFactory - algorithm_prefix = parts[0].lower() - if algorithm_prefix == 'md5': - return HashType( - alg=HashAlgorithm.MD5, - content=parts[1].lower() - ) - elif algorithm_prefix[0:4] == 'sha3': - return HashType( - alg=getattr(HashAlgorithm, f'SHA3_{algorithm_prefix[5:]}'), - content=parts[1].lower() - ) - elif algorithm_prefix == 'sha1': - return HashType( - alg=HashAlgorithm.SHA_1, - content=parts[1].lower() - ) - elif algorithm_prefix[0:3] == 'sha': - # This is actually SHA2... - return HashType( - alg=getattr(HashAlgorithm, f'SHA_{algorithm_prefix[3:]}'), - content=parts[1].lower() - ) - elif algorithm_prefix[0:7] == 'blake2b': - return HashType( - alg=getattr(HashAlgorithm, f'BLAKE2B_{algorithm_prefix[7:]}'), - content=parts[1].lower() - ) - elif algorithm_prefix[0:6] == 'blake2': - return HashType( - alg=getattr(HashAlgorithm, f'BLAKE2B_{algorithm_prefix[6:]}'), - content=parts[1].lower() - ) - elif algorithm_prefix[0:6] == 'blake3': - return HashType( - alg=HashAlgorithm.BLAKE3, - content=parts[1].lower() - ) - raise UnknownHashTypeException(f'Unable to determine hash type from {composite_hash!r}') + return HashTypeFactory().from_composite_str(composite_hash) def __init__( self, *, diff --git a/cyclonedx/model/bom.py b/cyclonedx/model/bom.py index 9c4b8cad..08e83877 100644 --- a/cyclonedx/model/bom.py +++ b/cyclonedx/model/bom.py @@ -706,6 +706,8 @@ def get_component_by_purl(self, purl: Optional['PackageURL']) -> Optional[Compon Returns: `Component` or `None` + + .. deprecated:: next """ if purl: found = [x for x in self.components if x.purl == purl] @@ -720,6 +722,8 @@ def get_urn_uuid(self) -> str: Returns: URN formatted UUID that uniquely identified this Bom instance. + + .. deprecated:: next """ return self.serial_number.urn @@ -733,6 +737,8 @@ def has_component(self, component: Component) -> bool: Returns: `bool` - `True` if the supplied Component is part of this Bom, `False` otherwise. + + .. deprecated:: next """ return component in self.components @@ -751,8 +757,10 @@ def get_vulnerabilities_for_bom_ref(self, bom_ref: BomRef) -> 'SortedSet[Vulnera Returns: `SortedSet` of `Vulnerability` - """ + .. deprecated:: next + Deprecated without any replacement. + """ vulnerabilities: SortedSet[Vulnerability] = SortedSet() for v in self.vulnerabilities: for target in v.affects: @@ -766,6 +774,9 @@ def has_vulnerabilities(self) -> bool: Returns: `bool` - `True` if this Bom has at least one Vulnerability, `False` otherwise. + + .. deprecated:: next + Deprecated without any replacement. """ return bool(self.vulnerabilities) @@ -788,6 +799,11 @@ def register_dependency(self, target: Dependable, depends_on: Optional[Iterable[ self.register_dependency(target=_d2, depends_on=None) def urn(self) -> str: + """ + .. deprecated:: next + Deprecated without any replacement. + """ + # idea: have 'serial_number' be a string, and use it instead of this method return f'{_BOM_LINK_PREFIX}{self.serial_number}/{self.version}' def validate(self) -> bool: @@ -797,7 +813,11 @@ def validate(self) -> bool: Returns: `bool` + + .. deprecated:: next + Deprecated without any replacement. """ + # !! deprecated function. have this as an part of the normalization process, like the BomRefDiscrimator # 0. Make sure all Dependable have a Dependency entry if self.metadata.component: self.register_dependency(target=self.metadata.component) diff --git a/cyclonedx/model/component.py b/cyclonedx/model/component.py index c4e4e5c6..47702ed0 100644 --- a/cyclonedx/model/component.py +++ b/cyclonedx/model/component.py @@ -16,12 +16,17 @@ # Copyright (c) OWASP Foundation. All Rights Reserved. import re +import sys from collections.abc import Iterable from enum import Enum -from os.path import exists from typing import Any, Optional, Union from warnings import warn +if sys.version_info >= (3, 13): + from warnings import deprecated +else: + from typing_extensions import deprecated + # See https://github.com/package-url/packageurl-python/issues/65 import py_serializable as serializable from packageurl import PackageURL @@ -29,7 +34,6 @@ from .._internal.bom_ref import bom_ref_from_str as _bom_ref_from_str from .._internal.compare import ComparablePackageURL as _ComparablePackageURL, ComparableTuple as _ComparableTuple -from .._internal.hash import file_sha1sum as _file_sha1sum from ..exception.model import InvalidOmniBorIdException, InvalidSwhidException from ..exception.serialization import ( CycloneDxDeserializationException, @@ -955,34 +959,26 @@ class Component(Dependable): """ @staticmethod + @deprecated('Deprecated - use cyclonedx.contrib.component.builders.ComponentBuilder().make_for_file() instead') def for_file(absolute_file_path: str, path_for_bom: Optional[str]) -> 'Component': - """ + """Deprecated — Wrapper of :func:`cyclonedx.contrib.component.builders.ComponentBuilder.make_for_file`. + Helper method to create a Component that represents the provided local file as a Component. - Args: - absolute_file_path: - Absolute path to the file you wish to represent - path_for_bom: - Optionally, if supplied this is the path that will be used to identify the file in the BOM + .. deprecated:: next + Use ``cyclonedx.contrib.component.builders.ComponentBuilder().make_for_file()`` instead. + """ + from ..contrib.component.builders import ComponentBuilder - Returns: - `Component` representing the supplied file - """ - if not exists(absolute_file_path): - raise FileExistsError(f'Supplied file path {absolute_file_path!r} does not exist') - - sha1_hash: str = _file_sha1sum(absolute_file_path) - return Component( - name=path_for_bom if path_for_bom else absolute_file_path, - version=f'0.0.0-{sha1_hash[0:12]}', - hashes=[ - HashType(alg=HashAlgorithm.SHA_1, content=sha1_hash) - ], - type=ComponentType.FILE, purl=PackageURL( - type='generic', name=path_for_bom if path_for_bom else absolute_file_path, - version=f'0.0.0-{sha1_hash[0:12]}' - ) + component = ComponentBuilder().make_for_file(absolute_file_path, name=path_for_bom) + sha1_hash = next((h.content for h in component.hashes if h.alg is HashAlgorithm.SHA_1), None) + assert sha1_hash is not None + component.version = f'0.0.0-{sha1_hash[0:12]}' + component.purl = PackageURL( # DEPRECATED: a file has no PURL! + type='generic', name=path_for_bom if path_for_bom else absolute_file_path, + version=f'0.0.0-{sha1_hash[0:12]}' ) + return component def __init__( self, *, diff --git a/cyclonedx/model/license.py b/cyclonedx/model/license.py index 3d936942..9d32160e 100644 --- a/cyclonedx/model/license.py +++ b/cyclonedx/model/license.py @@ -59,7 +59,11 @@ class LicenseAcknowledgement(str, Enum): # 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`""" +"""Deprecated — Alias for :class:`LicenseAcknowledgement` + +.. deprecated:: next Import `LicenseAcknowledgement` instead. + The exported original symbol itself is NOT deprecated - only this import path. +""" @serializable.serializable_class( diff --git a/cyclonedx/model/vulnerability.py b/cyclonedx/model/vulnerability.py index fcfb4a68..5f3bf024 100644 --- a/cyclonedx/model/vulnerability.py +++ b/cyclonedx/model/vulnerability.py @@ -28,14 +28,19 @@ See the CycloneDX Schema extension definition https://cyclonedx.org/docs/1.7/xml/#type_vulnerabilitiesType """ - import re +import sys from collections.abc import Iterable from datetime import datetime from decimal import Decimal from enum import Enum from typing import Any, Optional, Union +if sys.version_info >= (3, 13): + from warnings import deprecated +else: + from typing_extensions import deprecated + import py_serializable as serializable from sortedcontainers import SortedSet @@ -724,38 +729,18 @@ class VulnerabilitySeverity(str, Enum): UNKNOWN = 'unknown' @staticmethod + @deprecated('Deprecated - use cyclonedx.contrib.vulnerability.cvss.vs_from_cvss_scores instead') def get_from_cvss_scores(scores: Union[tuple[float, ...], float, None]) -> 'VulnerabilitySeverity': - """ + """Deprecated — Alias of :func:`cyclonedx.contrib.vulnerability.cvss.vs_from_cvss_scores()`. + Derives the Severity of a Vulnerability from it's declared CVSS scores. - Args: - scores: A `tuple` of CVSS scores. CVSS scoring system allows for up to three separate scores. + .. deprecated:: next + Use ``cyclonedx.contrib.vulnerability.cvss.vs_from_cvss_scores()`` instead. + """ + from ..contrib.vulnerability.cvss import vs_from_cvss_scores - Returns: - Always returns an instance of `VulnerabilitySeverity`. - """ - if type(scores) is float: - scores = (scores,) - - if scores is None: - return VulnerabilitySeverity.UNKNOWN - - max_cvss_score: float - if isinstance(scores, tuple): - max_cvss_score = max(scores) - else: - max_cvss_score = float(scores) - - if max_cvss_score >= 9.0: - return VulnerabilitySeverity.CRITICAL - elif max_cvss_score >= 7.0: - return VulnerabilitySeverity.HIGH - elif max_cvss_score >= 4.0: - return VulnerabilitySeverity.MEDIUM - elif max_cvss_score > 0.0: - return VulnerabilitySeverity.LOW - else: - return VulnerabilitySeverity.NONE + return vs_from_cvss_scores(scores) @serializable.serializable_class(ignore_unknown_during_deserialization=True) diff --git a/examples/complex_serialize.py b/examples/complex_serialize.py index 03619f8b..a7c162bb 100644 --- a/examples/complex_serialize.py +++ b/examples/complex_serialize.py @@ -20,9 +20,9 @@ from packageurl import PackageURL -from cyclonedx.builder.this import this_component as cdx_lib_component +from cyclonedx.contrib.license.factories import LicenseFactory +from cyclonedx.contrib.this.builders import this_component as cdx_lib_component from cyclonedx.exception import MissingOptionalDependencyException -from cyclonedx.factory.license import LicenseFactory from cyclonedx.model import XsUri from cyclonedx.model.bom import Bom from cyclonedx.model.component import Component, ComponentType diff --git a/tests/test_component.py b/tests/test_component.py index 05ee373f..8e6e7e4d 100644 --- a/tests/test_component.py +++ b/tests/test_component.py @@ -21,7 +21,7 @@ # See https://github.com/package-url/packageurl-python/issues/65 from packageurl import PackageURL -from cyclonedx._internal.hash import file_sha1sum as _file_sha1sum +from cyclonedx.contrib.component.builders import ComponentBuilder from cyclonedx.model.component import Component from tests import OWN_DATA_DIRECTORY from tests._data.models import get_component_setuptools_simple @@ -65,7 +65,7 @@ def test_purl_incorrect_name(self) -> None: def test_from_xml_file_with_path_for_bom(self) -> None: test_file = join(OWN_DATA_DIRECTORY, 'xml', '1.4', 'bom_setuptools.xml') c = Component.for_file(absolute_file_path=test_file, path_for_bom='fixtures/bom_setuptools.xml') - sha1_hash: str = _file_sha1sum(filename=test_file) + sha1_hash: str = ComponentBuilder._file_sha1sum(filename=test_file) expected_version = f'0.0.0-{sha1_hash[0:12]}' self.assertEqual(c.name, 'fixtures/bom_setuptools.xml') self.assertEqual(c.version, expected_version) diff --git a/tests/test_contrib/.gitkeep b/tests/test_contrib/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/tests/test_deserialize_json.py b/tests/test_deserialize_json.py index 14fcb953..85f8b11a 100644 --- a/tests/test_deserialize_json.py +++ b/tests/test_deserialize_json.py @@ -43,7 +43,7 @@ class TestDeserializeJson(TestCase, SnapshotMixin, DeepCompareMixin): @named_data(*all_get_bom_funct_valid_immut, *all_get_bom_funct_valid_reversible_migrate) - @patch('cyclonedx.builder.this.__ThisVersion', 'TESTING') + @patch('cyclonedx.contrib.this.builders.__ThisVersion', 'TESTING') def test_prepared(self, get_bom: Callable[[], Bom], *_: Any, **__: Any) -> None: # only latest schema will have all data populated in serialized form snapshot_name = mksname(get_bom, _LATEST_SCHEMA, OutputFormat.JSON) diff --git a/tests/test_deserialize_xml.py b/tests/test_deserialize_xml.py index 12e32e5f..8ad0f31c 100644 --- a/tests/test_deserialize_xml.py +++ b/tests/test_deserialize_xml.py @@ -40,7 +40,7 @@ class TestDeserializeXml(TestCase, SnapshotMixin, DeepCompareMixin): @named_data(*all_get_bom_funct_valid_immut, *all_get_bom_funct_valid_reversible_migrate) - @patch('cyclonedx.builder.this.__ThisVersion', 'TESTING') + @patch('cyclonedx.contrib.this.builders.__ThisVersion', 'TESTING') def test_prepared(self, get_bom: Callable[[], Bom], *_: Any, **__: Any) -> None: # only latest schema will have all data populated in serialized form snapshot_name = mksname(get_bom, _LATEST_SCHEMA, OutputFormat.XML) diff --git a/tests/test_factory_license.py b/tests/test_factory_license.py index 051a88b3..d381347b 100644 --- a/tests/test_factory_license.py +++ b/tests/test_factory_license.py @@ -32,8 +32,8 @@ def test_make_from_string_with_id(self) -> None: acknowledgement = unittest.mock.NonCallableMock(spec=LicenseAcknowledgement) expected = DisjunctiveLicense(id='bar', text=text, url=url, acknowledgement=acknowledgement) - with unittest.mock.patch('cyclonedx.factory.license.spdx_fixup', return_value='bar'), \ - unittest.mock.patch('cyclonedx.factory.license.is_spdx_expression', return_value=True): + with unittest.mock.patch('cyclonedx.contrib.license.factories.spdx_fixup', return_value='bar'), \ + unittest.mock.patch('cyclonedx.contrib.license.factories.is_spdx_expression', return_value=True): actual = LicenseFactory().make_from_string('foo', license_text=text, license_url=url, @@ -47,8 +47,8 @@ def test_make_from_string_with_name(self) -> None: acknowledgement = unittest.mock.NonCallableMock(spec=LicenseAcknowledgement) expected = DisjunctiveLicense(name='foo', text=text, url=url, acknowledgement=acknowledgement) - with unittest.mock.patch('cyclonedx.factory.license.spdx_fixup', return_value=None), \ - unittest.mock.patch('cyclonedx.factory.license.is_spdx_expression', return_value=False): + with unittest.mock.patch('cyclonedx.contrib.license.factories.spdx_fixup', return_value=None), \ + unittest.mock.patch('cyclonedx.contrib.license.factories.is_spdx_expression', return_value=False): actual = LicenseFactory().make_from_string('foo', license_text=text, license_url=url, @@ -60,8 +60,8 @@ def test_make_from_string_with_expression(self) -> None: acknowledgement = unittest.mock.NonCallableMock(spec=LicenseAcknowledgement) expected = LicenseExpression('foo', acknowledgement=acknowledgement) - with unittest.mock.patch('cyclonedx.factory.license.spdx_fixup', return_value=None), \ - unittest.mock.patch('cyclonedx.factory.license.is_spdx_expression', return_value=True): + with unittest.mock.patch('cyclonedx.contrib.license.factories.spdx_fixup', return_value=None), \ + unittest.mock.patch('cyclonedx.contrib.license.factories.is_spdx_expression', return_value=True): actual = LicenseFactory().make_from_string('foo', license_acknowledgement=acknowledgement) @@ -73,14 +73,14 @@ def test_make_with_id(self) -> None: acknowledgement = unittest.mock.NonCallableMock(spec=LicenseAcknowledgement) expected = DisjunctiveLicense(id='bar', text=text, url=url, acknowledgement=acknowledgement) - with unittest.mock.patch('cyclonedx.factory.license.spdx_fixup', return_value='bar'): + with unittest.mock.patch('cyclonedx.contrib.license.factories.spdx_fixup', return_value='bar'): actual = LicenseFactory().make_with_id(spdx_id='foo', text=text, url=url, acknowledgement=acknowledgement) self.assertEqual(expected, actual) def test_make_with_id_raises(self) -> None: with self.assertRaises(InvalidSpdxLicenseException, msg='foo'): - with unittest.mock.patch('cyclonedx.factory.license.spdx_fixup', return_value=None): + with unittest.mock.patch('cyclonedx.contrib.license.factories.spdx_fixup', return_value=None): LicenseFactory().make_with_id(spdx_id='foo') def test_make_with_name(self) -> None: @@ -94,11 +94,11 @@ def test_make_with_name(self) -> None: def test_make_with_expression(self) -> None: acknowledgement = unittest.mock.NonCallableMock(spec=LicenseAcknowledgement) expected = LicenseExpression('foo', acknowledgement=acknowledgement) - with unittest.mock.patch('cyclonedx.factory.license.is_spdx_expression', return_value=True): + with unittest.mock.patch('cyclonedx.contrib.license.factories.is_spdx_expression', return_value=True): actual = LicenseFactory().make_with_expression(expression='foo', acknowledgement=acknowledgement) self.assertEqual(expected, actual) def test_make_with_expression_raises(self) -> None: with self.assertRaises(InvalidLicenseExpressionException, msg='foo'): - with unittest.mock.patch('cyclonedx.factory.license.is_spdx_expression', return_value=False): + with unittest.mock.patch('cyclonedx.contrib.license.factories.is_spdx_expression', return_value=False): LicenseFactory().make_with_expression('foo') diff --git a/tests/test_output_json.py b/tests/test_output_json.py index 0bc3f1e2..b9340a4e 100644 --- a/tests/test_output_json.py +++ b/tests/test_output_json.py @@ -62,7 +62,7 @@ def test_unsupported_schema_raises(self, sv: SchemaVersion) -> None: and is_valid_for_schema_version(gb, sv) )) @unpack - @patch('cyclonedx.builder.this.__ThisVersion', 'TESTING') + @patch('cyclonedx.contrib.this.builders.__ThisVersion', 'TESTING') def test_valid(self, get_bom: Callable[[], Bom], sv: SchemaVersion, *_: Any, **__: Any) -> None: snapshot_name = mksname(get_bom, sv, OutputFormat.JSON) bom = get_bom() diff --git a/tests/test_output_xml.py b/tests/test_output_xml.py index 2b1a16ee..6e887ded 100644 --- a/tests/test_output_xml.py +++ b/tests/test_output_xml.py @@ -49,7 +49,7 @@ class TestOutputXml(TestCase, SnapshotMixin): if is_valid_for_schema_version(gb, sv) )) @unpack - @patch('cyclonedx.builder.this.__ThisVersion', 'TESTING') + @patch('cyclonedx.contrib.this.builders.__ThisVersion', 'TESTING') def test_valid(self, get_bom: Callable[[], Bom], sv: SchemaVersion, *_: Any, **__: Any) -> None: snapshot_name = mksname(get_bom, sv, OutputFormat.XML) if snapshot_name is None: diff --git a/tests/test_real_world_examples.py b/tests/test_real_world_examples.py index 3170e730..232e3291 100644 --- a/tests/test_real_world_examples.py +++ b/tests/test_real_world_examples.py @@ -26,7 +26,7 @@ from tests import OWN_DATA_DIRECTORY -@patch('cyclonedx.builder.this.__ThisVersion', 'TESTING') +@patch('cyclonedx.contrib.this.builders.__ThisVersion', 'TESTING') @patch('cyclonedx.model.bom._get_now_utc', return_value=datetime.fromisoformat('2023-01-07 13:44:32.312678+00:00')) class TestDeserializeRealWorldExamples(unittest.TestCase):