Skip to content

Commit

Permalink
feat!: v5.0.0 (#797)
Browse files Browse the repository at this point in the history
### BREAKING Changes

* Emitted metadata tool name is `cyclonedx-py`, was `cyclonedx-bom`.
* Emitted metadata tools are up to non-deprecated CycloneDX specification.
* No longer emit deprecated or undocumented properties in namespace [`cdx:poetry`](https://github.com/CycloneDX/cyclonedx-property-taxonomy/blob/main/cdx/poetry.md) (see previous release 4.6.0 for official replacements).
    - `cdx:poetry:source:package:reference`
    - `cdx:poetry:package:source:resolved_reference`
    - `cdx:poetry:package:source:vcs:requested_revision`
    - `cdx:poetry:package:source:vcs:commit_id`

The mentioned changes are considered "breaking" for processes that relied on the respective data structures. 
Migration paths are self-explanatory.

### Dependencies

* Requires `cyclonedx-python-lib>=8.0.0,<9 ` now, was `>=7.3.0,<8.0.0,!=7.3.1`.


---------

Signed-off-by: Jan Kowalleck <[email protected]>
  • Loading branch information
jkowalleck authored Oct 15, 2024
1 parent 6707959 commit 34cf6e3
Show file tree
Hide file tree
Showing 919 changed files with 23,999 additions and 9,019 deletions.
7 changes: 0 additions & 7 deletions cyclonedx_py/_internal/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,13 +71,6 @@ class PropertyName(Enum):
# region poetry
# see https://github.com/CycloneDX/cyclonedx-property-taxonomy/blob/main/cdx/poetry.md
PoetryGroup = 'cdx:poetry:group'
# region poetry-deprecated
# the following property names are deprecated
PoetryPackageSourceReference_misspelled = 'cdx:poetry:source:package:reference'
PoetryPackageSourceResolvedReference = 'cdx:poetry:package:source:resolved_reference'
PoetryPackageSourceVcsRequestedRevision = 'cdx:poetry:package:source:vcs:requested_revision'
PoetryPackageSourceVcsCommitId = 'cdx:poetry:package:source:vcs:commit_id'
# endregion poetry-deprecated
# endregion poetry

# region pipenv
Expand Down
6 changes: 0 additions & 6 deletions cyclonedx_py/_internal/environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,16 +245,10 @@ def __component_add_extref_and_purl(self, component: 'Component',
component.properties.add(Property(
name=PropertyName.PythonPackageSourceVcsCommitId.value,
value=packagesource.commit_id))
component.properties.add(Property(
name=PropertyName.PoetryPackageSourceVcsCommitId.value, # deprecated
value=packagesource.commit_id))
if packagesource.requested_revision:
component.properties.add(Property(
name=PropertyName.PythonPackageSourceVcsRequestedRevision.value,
value=packagesource.requested_revision))
component.properties.add(Property(
name=PropertyName.PoetryPackageSourceVcsRequestedRevision.value, # deprecated
value=packagesource.requested_revision))
elif isinstance(packagesource, PackageSourceArchive):
if '://files.pythonhosted.org/' not in packagesource.url:
# skip PURL bloat, do not add implicit information
Expand Down
10 changes: 0 additions & 10 deletions cyclonedx_py/_internal/poetry.py
Original file line number Diff line number Diff line change
Expand Up @@ -416,16 +416,6 @@ def __make_component4lock(self, package: 'T_NameDict') -> 'Component':
name=PropertyName.PoetryGroup.value,
value=package['category']
) if 'category' in package else None,
# region deprecated
Property(
name=PropertyName.PoetryPackageSourceReference_misspelled.value, # deprecated
value=source['reference']
) if is_vcs and 'reference' in source else None,
Property(
name=PropertyName.PoetryPackageSourceResolvedReference.value, # deprecated
value=source['resolved_reference']
) if is_vcs and 'resolved_reference' in source else None,
# endregion deprecated
)),
purl=PackageURL(
type=PurlTypePypi,
Expand Down
16 changes: 16 additions & 0 deletions cyclonedx_py/_internal/utils/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# This file is part of CycloneDX Python
#
# 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.
95 changes: 54 additions & 41 deletions cyclonedx_py/_internal/utils/cdx.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,54 +23,67 @@
from re import compile as re_compile
from typing import Any, Dict, Iterable

from cyclonedx.model import ExternalReference, ExternalReferenceType, Tool, XsUri
from cyclonedx.builder.this import this_component as lib_component
from cyclonedx.model import ExternalReference, ExternalReferenceType, XsUri
from cyclonedx.model.bom import Bom
from cyclonedx.model.license import License, LicenseExpression
from cyclonedx.model.component import Component, ComponentType
from cyclonedx.model.license import DisjunctiveLicense, License, LicenseAcknowledgement, LicenseExpression

from cyclonedx_py import __version__
from ... import __version__ as __THIS_VERSION # noqa:N812


def make_bom(**kwargs: Any) -> Bom:
bom = Bom(**kwargs)
bom.metadata.tools.add(Tool(
# keep in sync with `../../../pyproject.toml`
vendor='CycloneDX',
name='cyclonedx-bom',
version=__version__,
external_references=[
ExternalReference(
type=ExternalReferenceType.BUILD_SYSTEM,
url=XsUri('https://github.com/CycloneDX/cyclonedx-python/actions')
bom.metadata.tools.components.update((
lib_component(),
Component(
type=ComponentType.APPLICATION,
group='CycloneDX',
# package is called 'cyclonedx-bom', but the tool is called 'cyclonedx-py'
name='cyclonedx-py',
version=__THIS_VERSION,
description='CycloneDX Software Bill of Materials (SBOM) generator for Python projects and environments',
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/#readme')
),
ExternalReference(
type=ExternalReferenceType.DOCUMENTATION,
url=XsUri('https://cyclonedx-bom-tool.readthedocs.io/')
),
ExternalReference(
type=ExternalReferenceType.VCS,
url=XsUri('https://github.com/CycloneDX/cyclonedx-python/')
),
ExternalReference(
type=ExternalReferenceType.BUILD_SYSTEM,
url=XsUri('https://github.com/CycloneDX/cyclonedx-python/actions')
),
ExternalReference(
type=ExternalReferenceType.ISSUE_TRACKER,
url=XsUri('https://github.com/CycloneDX/cyclonedx-python/issues')
),
ExternalReference(
type=ExternalReferenceType.LICENSE,
url=XsUri('https://github.com/CycloneDX/cyclonedx-python/blob/main/LICENSE')
),
ExternalReference(
type=ExternalReferenceType.RELEASE_NOTES,
url=XsUri('https://github.com/CycloneDX/cyclonedx-python/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-bom/')
),
),
ExternalReference(
type=ExternalReferenceType.DISTRIBUTION,
url=XsUri('https://pypi.org/project/cyclonedx-bom/')
),
ExternalReference(
type=ExternalReferenceType.DOCUMENTATION,
url=XsUri('https://cyclonedx-bom-tool.readthedocs.io/')
),
ExternalReference(
type=ExternalReferenceType.ISSUE_TRACKER,
url=XsUri('https://github.com/CycloneDX/cyclonedx-python/issues')
),
ExternalReference(
type=ExternalReferenceType.LICENSE,
url=XsUri('https://github.com/CycloneDX/cyclonedx-python/blob/main/LICENSE')
),
ExternalReference(
type=ExternalReferenceType.RELEASE_NOTES,
url=XsUri('https://github.com/CycloneDX/cyclonedx-python/blob/main/CHANGELOG.md')
),
ExternalReference(
type=ExternalReferenceType.VCS,
url=XsUri('https://github.com/CycloneDX/cyclonedx-python/')
),
ExternalReference(
type=ExternalReferenceType.WEBSITE,
url=XsUri('https://github.com/CycloneDX/cyclonedx-python/#readme')
)
]))
# to be extended...
),
))
return bom


Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ build-backend = "poetry.core.masonry.api"


[tool.poetry]
# keep in sync with `cyclonedx_py/_internal/utils/cdx.py`
name = "cyclonedx-bom"
version = "4.6.1"
description = "CycloneDX Software Bill of Materials (SBOM) generator for Python projects and environments"
Expand Down Expand Up @@ -69,7 +68,7 @@ cyclonedx-py = "cyclonedx_py._internal.cli:run"

[tool.poetry.dependencies]
python = "^3.8"
cyclonedx-python-lib = { version = "^7.3.0, !=7.3.1", extras = ["validation"] }
cyclonedx-python-lib = { version = "^8.0", extras = ["validation"] }
packageurl-python = ">=0.11, <2" # keep in sync with same dep in `cyclonedx-python-lib`
pip-requirements-parser = "^32.0"
packaging = "^22 || ^23 || ^24"
Expand All @@ -92,6 +91,7 @@ isort = "5.13.2"
autopep8 = "2.3.1"
mypy = "1.11.2"
bandit = "1.7.10"
tomli = { version = "^2.0.1", python = "<3.11" }
tox = "4.21.2"
# min version required to be able to install some dependencies
# see https://github.com/MichaelKim0407/flake8-use-fstring/issues/33
Expand Down
120 changes: 93 additions & 27 deletions tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,12 @@
# SPDX-License-Identifier: Apache-2.0
# Copyright (c) OWASP Foundation. All Rights Reserved.


import sys
from json import dumps as json_dumps
from os import getenv
from os.path import dirname, join
from os import getenv, path
from pathlib import Path
from re import sub as re_sub
from sys import stderr
from typing import Union
from typing import Any, Dict, Union
from unittest import TestCase
from xml.sax.saxutils import escape as xml_escape, quoteattr as xml_quoteattr # nosec:B406

Expand All @@ -32,16 +30,16 @@

RECREATE_SNAPSHOTS = '1' == getenv('CDX_TEST_RECREATE_SNAPSHOTS')
if RECREATE_SNAPSHOTS:
print('!!! WILL RECREATE ALL SNAPSHOTS !!!', file=stderr)
print('!!! WILL RECREATE ALL SNAPSHOTS !!!', file=sys.stderr)

INIT_TESTBEDS = '1' != getenv('CDX_TEST_SKIP_INIT_TESTBEDS')
if INIT_TESTBEDS:
print('!!! WILL INIT TESTBEDS !!!', file=stderr)
print('!!! WILL INIT TESTBEDS !!!', file=sys.stderr)

_TESTDATA_DIRECTORY = join(dirname(__file__), '_data')
_TESTDATA_DIRECTORY = path.join(path.dirname(__file__), '_data')

INFILES_DIRECTORY = join(_TESTDATA_DIRECTORY, 'infiles')
SNAPSHOTS_DIRECTORY = join(_TESTDATA_DIRECTORY, 'snapshots')
INFILES_DIRECTORY = path.join(_TESTDATA_DIRECTORY, 'infiles')
SNAPSHOTS_DIRECTORY = path.join(_TESTDATA_DIRECTORY, 'snapshots')

UNSUPPORTED_OF_SV = (
(OutputFormat.JSON, SchemaVersion.V1_1),
Expand All @@ -60,7 +58,7 @@ class SnapshotMixin:

@staticmethod
def getSnapshotFile(snapshot_name: str) -> str: # noqa: N802
return join(SNAPSHOTS_DIRECTORY, f'{snapshot_name}.bin')
return path.join(SNAPSHOTS_DIRECTORY, f'{snapshot_name}.bin')

@classmethod
def writeSnapshot(cls, snapshot_name: str, data: str) -> None: # noqa: N802
Expand Down Expand Up @@ -92,62 +90,121 @@ def assertEqualSnapshot(self: Union[TestCase, 'SnapshotMixin'], # noqa: N802
_root_file_uri_xml_attr = xml_quoteattr(_root_file_uri)[1:-1]
_root_file_uri_json = json_dumps(_root_file_uri)[1:-1]

# package is called 'cyclonedx-bom', but the tool is called 'cyclonedx-py'
EXPECTED_TOOL_NAME = 'cyclonedx-py'


def make_xml_comparable(bom: str) -> str:
bom = bom.replace(_root_file_uri_xml, 'file://.../')
bom = bom.replace(_root_file_uri_xml_attr, 'file://.../')
bom = bom.replace( # replace metadata.tools.version
bom = bom.replace( # replace this version in metadata.tools.components
' <group>CycloneDX</group>\n'
f' <name>{EXPECTED_TOOL_NAME}</name>\n'
f' <version>{__this_version}</version>',
' <group>CycloneDX</group>\n'
f' <name>{EXPECTED_TOOL_NAME}</name>\n'
' <version>thisVersion-testing</version>')
bom = bom.replace( # replace this version in metadata.tools
' <vendor>CycloneDX</vendor>\n'
' <name>cyclonedx-bom</name>\n'
f' <name>{EXPECTED_TOOL_NAME}</name>\n'
f' <version>{__this_version}</version>',
' <vendor>CycloneDX</vendor>\n'
' <name>cyclonedx-bom</name>\n'
f' <name>{EXPECTED_TOOL_NAME}</name>\n'
' <version>thisVersion-testing</version>')
bom = re_sub( # replace metadata.tools.version
bom = re_sub( # replace lib-dynamics in metadata.tools.components
' <group>CycloneDX</group>\n'
' <name>cyclonedx-python-lib</name>\n'
' <version>.*?</version>\n'
' <description>.*?</description>\n'
' <licenses>\n'
'(?: .*?\n)*'
' </licenses>\n'
' <externalReferences>\n'
'(?: .*?\n)*'
' </externalReferences>',
' <group>CycloneDX</group>\n'
' <name>cyclonedx-python-lib</name>\n'
' <version>libVersion-testing</version>\n'
' <description><!-- stripped --></description>\n'
' <licenses><!-- stripped --></licenses>\n'
' <externalReferences><!-- stripped --></externalReferences>',
bom)
bom = re_sub( # replace lib-dynamics version in metadata.tools[]
' <vendor>CycloneDX</vendor>\n'
' <name>cyclonedx-python-lib</name>\n'
' <version>.*?</version>',
' <vendor>CycloneDX</vendor>\n'
' <name>cyclonedx-python-lib</name>\n'
' <version>libVersion-testing</version>',
bom)
bom = re_sub( # replace metadata.tools.externalReferences
bom = re_sub( # replace lib-dynamics externalReferences in metadata.tools[]
' <vendor>CycloneDX</vendor>\n'
' <name>cyclonedx-python-lib</name>\n'
r' <version>(.*?)</version>\n'
r' <externalReferences>[\s\S]*?</externalReferences>',
' <version>(.*?)</version>\n'
' <externalReferences>\n'
'(?: .*?\n)*'
' </externalReferences>',
' <vendor>CycloneDX</vendor>\n'
' <name>cyclonedx-python-lib</name>\n'
r' <version>\1</version>''\n'
' <version>\\1</version>\n'
' <externalReferences><!-- stripped --></externalReferences>',
bom)
return bom


def make_json_comparable(bom: str) -> str:
bom = bom.replace(_root_file_uri_json, 'file://.../')
bom = bom.replace( # replace metadata.tools.version
' "name": "cyclonedx-bom",\n'
bom = bom.replace( # replace this version in metadata.tools.components[]
f' "name": {json_dumps(EXPECTED_TOOL_NAME)},\n'
' "type": "application",\n'
f' "version": {json_dumps(__this_version)}',
f' "name": {json_dumps(EXPECTED_TOOL_NAME)},\n'
' "type": "application",\n'
' "version": "thisVersion-testing"')
bom = bom.replace( # replace this version in metadata.tools[]
f' "name": {json_dumps(EXPECTED_TOOL_NAME)},\n'
' "vendor": "CycloneDX",\n'
f' "version": {json_dumps(__this_version)}',
' "name": "cyclonedx-bom",\n'
f' "name": {json_dumps(EXPECTED_TOOL_NAME)},\n'
' "vendor": "CycloneDX",\n'
' "version": "thisVersion-testing"')
bom = re_sub( # replace metadata.tools.version
bom = re_sub( # replace lib-dynamics in metadata.tools.components[]
' "description": ".*?",\n'
' "externalReferences": \\[\n'
'(?: .*?\n)*'
' \\],\n'
' "group": "CycloneDX",\n'
' "licenses": \\[\n'
'(?: .*?\n)*'
' \\],\n'
' "name": "cyclonedx-python-lib",\n'
' "type": "library",\n'
' "version": ".*?"',
' "description": "stripped",\n'
' "externalReferences": [ ],\n'
' "group": "CycloneDX",\n'
' "licenses": [ ],\n'
' "name": "cyclonedx-python-lib",\n'
' "type": "library",\n'
' "version": "libVersion-testing"',
bom)
bom = re_sub( # replace lib-dynamics version in metadata.tools[]
' "name": "cyclonedx-python-lib",\n'
' "vendor": "CycloneDX",\n'
' "version": ".*?"',
' "name": "cyclonedx-python-lib",\n'
' "vendor": "CycloneDX",\n'
' "version": "libVersion-testing"',
bom)
bom = re_sub( # replace metadata.tools.externalReferences
r' "externalReferences": \[[\s\S]*?\],\n'
bom = re_sub( # replace lib-dynamics externalReferences in metadata.tools[]
' "externalReferences": \\[\n'
'(?: .*?\n)*'
' \\],\n'
' "name": "cyclonedx-python-lib",\n'
' "vendor": "CycloneDX"',
' "vendor": "CycloneDX",\n',
' "externalReferences": [ ],\n'
' "name": "cyclonedx-python-lib",\n'
' "vendor": "CycloneDX"',
' "vendor": "CycloneDX",\n',
bom)
return bom

Expand All @@ -160,3 +217,12 @@ def make_comparable(bom: str, of: OutputFormat) -> str:
raise NotImplementedError(f'unknown OutputFormat: {of!r}')

# endregion reproducible test results


def load_pyproject() -> Dict[str, Any]:
if sys.version_info >= (3, 11):
from tomllib import load as toml_load
else:
from tomli import load as toml_load
with open(path.join(path.dirname(__file__), '..', 'pyproject.toml'), 'rb') as f:
return toml_load(f)
Loading

0 comments on commit 34cf6e3

Please sign in to comment.