From b44d0a7a27a2b20f88733be2e36387e48610c776 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C5=A0oltis?= Date: Fri, 6 Dec 2024 21:16:52 +0100 Subject: [PATCH] yarn-v1: Create SBOM components from packages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After a successful pre-fetching of all packages, report all downloaded packages as components in the final SBOM. Create the `Component` object from each package based on package attributes. Dev packages should have `cdx:npm:package:development` property, that is added to the component if package is marked for development -> `dev` attribute is set to True. Move the rest of the unit test logic to `test_fetch_yarn_source` from its predecessor in yarn-berry implementation. closes https://github.com/containerbuildsystem/cachi2/issues/636 Signed-off-by: Michal Ĺ oltis --- .../package_managers/yarn_classic/main.py | 26 ++++++++- .../yarn_classic/test_main.py | 54 +++++++++++++++---- 2 files changed, 67 insertions(+), 13 deletions(-) diff --git a/cachi2/core/package_managers/yarn_classic/main.py b/cachi2/core/package_managers/yarn_classic/main.py index 63f512616..241b20243 100644 --- a/cachi2/core/package_managers/yarn_classic/main.py +++ b/cachi2/core/package_managers/yarn_classic/main.py @@ -5,6 +5,7 @@ from cachi2.core.errors import PackageManagerError, PackageRejected from cachi2.core.models.input import Request from cachi2.core.models.output import Component, EnvironmentVariable, RequestOutput +from cachi2.core.models.property_semantics import PropertySet from cachi2.core.package_managers.yarn.utils import ( VersionsRange, extract_yarn_version_from_env, @@ -40,14 +41,16 @@ def _ensure_mirror_dir_exists(output_dir: RootedPath) -> None: for package in request.yarn_classic_packages: package_path = request.source_dir.join_within_root(package.path) _ensure_mirror_dir_exists(request.output_dir) - _resolve_yarn_project(Project.from_source_dir(package_path), request.output_dir) + components.extend( + _resolve_yarn_project(Project.from_source_dir(package_path), request.output_dir) + ) return RequestOutput.from_obj_list( components, _generate_build_environment_variables(), project_files=[] ) -def _resolve_yarn_project(project: Project, output_dir: RootedPath) -> None: +def _resolve_yarn_project(project: Project, output_dir: RootedPath) -> list[Component]: """Process a request for a single yarn source directory.""" log.info(f"Fetching the yarn dependencies at the subpath {project.source_dir}") @@ -58,6 +61,25 @@ def _resolve_yarn_project(project: Project, output_dir: RootedPath) -> None: packages = resolve_packages(project, output_dir.join_within_root(MIRROR_DIR)) _verify_no_offline_mirror_collisions(packages) + return _create_sbom_components(packages) + + +def _create_sbom_components(packages: Iterable[YarnClassicPackage]) -> list[Component]: + """Create SBOM components from the given yarn packages.""" + result = [] + for package in packages: + properties = PropertySet(npm_development=package.dev).to_properties() + result.append( + Component( + name=package.name, + purl=package.purl, + version=package.version, + properties=properties, + ) + ) + + return result + def _fetch_dependencies(source_dir: RootedPath, env: dict[str, str]) -> None: """Fetch dependencies using 'yarn install'. diff --git a/tests/unit/package_managers/yarn_classic/test_main.py b/tests/unit/package_managers/yarn_classic/test_main.py index 05d35145b..da7478278 100644 --- a/tests/unit/package_managers/yarn_classic/test_main.py +++ b/tests/unit/package_managers/yarn_classic/test_main.py @@ -1,3 +1,4 @@ +import itertools import json from pathlib import Path from typing import Any, Iterable @@ -54,19 +55,48 @@ def test_generate_build_environment_variables( @pytest.mark.parametrize( - "input_request, components", - [ + "input_request, package_components", + ( pytest.param( [{"type": "yarn-classic", "path": "."}], - [], + [ + [ + Component( + name="foo", + purl="pkg:npm/foo@1.0.0", + version="1.0.0", + ), + Component(name="bar", purl="pkg:npm/bar@2.0.0", version="2.0.0"), + ], + ], id="single_input_package", ), pytest.param( [{"type": "yarn-classic", "path": "."}, {"type": "yarn-classic", "path": "./path"}], - [], + [ + [ + Component( + name="foo", + purl="pkg:npm/foo@1.0.0", + version="1.0.0", + ), + ], + [ + Component( + name="bar", + purl="pkg:npm/bar@2.0.0", + version="2.0.0", + ), + Component( + name="baz", + purl="pkg:npm/baz@3.0.0", + version="3.0.0", + ), + ], + ], id="multiple_input_packages", ), - ], + ), indirect=["input_request"], ) @mock.patch("cachi2.core.package_managers.yarn_classic.main._resolve_yarn_project") @@ -75,26 +105,28 @@ def test_fetch_yarn_source( mock_create_project: mock.Mock, mock_resolve_yarn: mock.Mock, input_request: Request, + package_components: list[Component], yarn_classic_env_variables: list[EnvironmentVariable], - components: list[Component], ) -> None: - expected_output = RequestOutput( - components=components, - build_config=BuildConfig(environment_variables=yarn_classic_env_variables), - ) package_dirs = [ input_request.source_dir.join_within_root(p.path) for p in input_request.packages ] projects = [_prepare_project(path, {}) for path in package_dirs] + mock_create_project.side_effect = projects + mock_resolve_yarn.side_effect = package_components output = fetch_yarn_source(input_request) mock_create_project.assert_has_calls([mock.call(path) for path in package_dirs]) mock_resolve_yarn.assert_has_calls([mock.call(p, input_request.output_dir) for p in projects]) - assert input_request.output_dir.join_within_root("deps/yarn-classic").path.exists() + expected_output = RequestOutput( + components=list(itertools.chain.from_iterable(package_components)), + build_config=BuildConfig(environment_variables=yarn_classic_env_variables), + ) assert output == expected_output + assert input_request.output_dir.join_within_root(MIRROR_DIR).path.exists() @mock.patch("cachi2.core.package_managers.yarn_classic.main.resolve_packages")