diff --git a/cyclonedx_py/_internal/environment.py b/cyclonedx_py/_internal/environment.py index c6dd5f9a..21a5a3e9 100644 --- a/cyclonedx_py/_internal/environment.py +++ b/cyclonedx_py/_internal/environment.py @@ -36,7 +36,7 @@ from . import BomBuilder, PropertyName, PurlTypePypi from .cli_common import add_argument_mc_type, add_argument_pyproject from .utils.cdx import licenses_fixup, make_bom -from .utils.packaging import metadata2extrefs, metadata2licenses, normalize_packagename +from .utils.packaging import metadata2extrefs, metadata2licenses, metadata2tags, normalize_packagename from .utils.pep610 import PackageSourceArchive, PackageSourceVcs, packagesource2extref, packagesource4dist from .utils.pep639 import dist2licenses_from_files as pep639_dist2licenses_from_files from .utils.pyproject import pyproject2component, pyproject2dependencies, pyproject_load @@ -185,6 +185,9 @@ def __add_components(self, bom: 'Bom', # path of dist-package on disc? naaa... a package may have multiple files/folders on disc ) + if hasattr(component, 'tags'): + component.tags.update(metadata2tags(dist_meta)) + # region licenses component.licenses.update(metadata2licenses(dist_meta, LicenseFactory(), gather_texts=self._gather_license_texts)) diff --git a/cyclonedx_py/_internal/pipenv.py b/cyclonedx_py/_internal/pipenv.py index 0c1d5997..15bbb2cf 100644 --- a/cyclonedx_py/_internal/pipenv.py +++ b/cyclonedx_py/_internal/pipenv.py @@ -33,7 +33,7 @@ from .cli_common import add_argument_mc_type, add_argument_pyproject from .utils.args import arparse_split from .utils.cdx import make_bom -from .utils.packaging import normalize_packagename +from .utils.packaging import normalize_packagename, to_tags from .utils.pyproject import pyproject_file2component from .utils.secret import redact_auth_from_url @@ -175,6 +175,8 @@ def _make_bom(self, root_c: Optional['Component'], version=package_data['version'][2:] if 'version' in package_data else None, external_references=self.__make_extrefs(package_name, package_data, source_urls), ) + if hasattr(component, 'tags'): + component.tags.update(to_tags(package_data.get('keywords'))) component.purl = PackageURL( type=PurlTypePypi, name=component.name, diff --git a/cyclonedx_py/_internal/poetry.py b/cyclonedx_py/_internal/poetry.py index d3eaa493..de47b164 100644 --- a/cyclonedx_py/_internal/poetry.py +++ b/cyclonedx_py/_internal/poetry.py @@ -34,7 +34,7 @@ from . import BomBuilder, PropertyName, PurlTypePypi from .cli_common import add_argument_mc_type from .utils.cdx import make_bom -from .utils.packaging import normalize_packagename +from .utils.packaging import normalize_packagename, to_tags from .utils.poetry import poetry2component from .utils.secret import redact_auth_from_url from .utils.toml import toml_loads @@ -404,7 +404,7 @@ def __make_component4lock(self, package: 'T_NameDict') -> 'Component': is_vcs = source.get('type') in self.__PACKAGE_SRC_VCS is_local = source.get('type') in self.__PACKAGE_SRC_LOCAL - return Component( + component = Component( bom_ref=f'{package["name"]}@{package["version"]}', name=package['name'], version=package.get('version'), @@ -433,6 +433,12 @@ def __make_component4lock(self, package: 'T_NameDict') -> 'Component': ) if not is_local else None ) + if hasattr(component, 'tags'): + component.tags.update(to_tags(package.get('keywords'))) + self._logger.debug('component created: %r', component) + + return component + def __purl_qualifiers4lock(self, package: 'T_NameDict') -> 'T_NameDict': # see https://github.com/package-url/purl-spec/blob/master/PURL-SPECIFICATION.rst qs = {} diff --git a/cyclonedx_py/_internal/utils/packaging.py b/cyclonedx_py/_internal/utils/packaging.py index 0b51003e..ce318ffd 100644 --- a/cyclonedx_py/_internal/utils/packaging.py +++ b/cyclonedx_py/_internal/utils/packaging.py @@ -33,6 +33,22 @@ from ..py_interop.packagemetadata import PackageMetadata +_KEYWORDS_SPLIT_MATCHER = re_compile(r'[;, ]+') + + +def metadata2tags(metadata: 'PackageMetadata') -> Generator[str, None, None]: + """ + Generate tags from metadata keywords. + """ + keywords_string = metadata.get('Keywords', '') + if keywords_string: + yield from ( + tag + for tag in (s.strip() for s in _KEYWORDS_SPLIT_MATCHER.split(keywords_string)) + if tag + ) + + def metadata2licenses(metadata: 'PackageMetadata', lfac: 'LicenseFactory', gather_texts: bool ) -> Generator['License', None, None]: