From 30b574dd316a2f63fb34714f37e572712945e3a4 Mon Sep 17 00:00:00 2001 From: Alexey Ovchinnikov Date: Tue, 3 Dec 2024 21:41:43 -0600 Subject: [PATCH] Adding optional RPM summary to SBOMs This change adds an option to include RPM summary in a SBOM. Signed-off-by: Alexey Ovchinnikov --- cachi2/core/models/input.py | 1 + cachi2/core/models/property_semantics.py | 8 ++ cachi2/core/models/sbom.py | 1 + cachi2/core/package_managers/rpm/main.py | 24 ++++- .../.build-config.yaml | 2 + .../rpm_summary_is_reported/bom.json | 91 +++++++++++++++++++ tests/integration/test_rpm.py | 16 ++++ tests/unit/models/test_input.py | 5 + tests/unit/package_managers/test_rpm.py | 2 +- 9 files changed, 145 insertions(+), 5 deletions(-) create mode 100644 tests/integration/test_data/rpm_summary_is_reported/.build-config.yaml create mode 100644 tests/integration/test_data/rpm_summary_is_reported/bom.json diff --git a/cachi2/core/models/input.py b/cachi2/core/models/input.py index 805b95d27..5b9381d35 100644 --- a/cachi2/core/models/input.py +++ b/cachi2/core/models/input.py @@ -214,6 +214,7 @@ class RpmPackageInput(_PackageInputBase): """Accepted input for a rpm package.""" type: Literal["rpm"] + include_summary_in_sbom: bool = False options: Optional[ExtraOptions] = None diff --git a/cachi2/core/models/property_semantics.py b/cachi2/core/models/property_semantics.py index 6261b9a77..11fb8a06f 100644 --- a/cachi2/core/models/property_semantics.py +++ b/cachi2/core/models/property_semantics.py @@ -34,6 +34,7 @@ class PropertySet: npm_development: bool = False pip_package_binary: bool = False bundler_package_binary: bool = False + rpm_summary: str = "" @classmethod def from_properties(cls, props: Iterable[Property]) -> "Self": @@ -44,6 +45,7 @@ def from_properties(cls, props: Iterable[Property]) -> "Self": npm_development = False pip_package_binary = False bundler_package_binary = False + rpm_summary = "" for prop in props: if prop.name == "cachi2:found_by": @@ -58,6 +60,8 @@ def from_properties(cls, props: Iterable[Property]) -> "Self": pip_package_binary = True elif prop.name == "cachi2:bundler:package:binary": bundler_package_binary = True + elif prop.name == "cachi2:rpm_summary": + rpm_summary = prop.value else: assert_never(prop.name) @@ -68,6 +72,7 @@ def from_properties(cls, props: Iterable[Property]) -> "Self": npm_development, pip_package_binary, bundler_package_binary, + rpm_summary, ) def to_properties(self) -> list[Property]: @@ -87,6 +92,8 @@ def to_properties(self) -> list[Property]: props.append(Property(name="cachi2:pip:package:binary", value="true")) if self.bundler_package_binary: props.append(Property(name="cachi2:bundler:package:binary", value="true")) + if self.rpm_summary: + props.append(Property(name="cachi2:rpm_summary", value=self.rpm_summary)) return sorted(props, key=lambda p: (p.name, p.value)) @@ -100,4 +107,5 @@ def merge(self, other: "Self") -> "Self": npm_development=self.npm_development and other.npm_development, pip_package_binary=self.pip_package_binary or other.pip_package_binary, bundler_package_binary=self.bundler_package_binary or other.bundler_package_binary, + rpm_summary=self.rpm_summary or other.rpm_summary, ) diff --git a/cachi2/core/models/sbom.py b/cachi2/core/models/sbom.py index 0152513e3..de9781f7e 100644 --- a/cachi2/core/models/sbom.py +++ b/cachi2/core/models/sbom.py @@ -7,6 +7,7 @@ PropertyName = Literal[ "cachi2:bundler:package:binary", "cachi2:found_by", + "cachi2:rpm_summary", "cachi2:missing_hash:in_file", "cachi2:pip:package:binary", "cdx:npm:package:bundled", diff --git a/cachi2/core/package_managers/rpm/main.py b/cachi2/core/package_managers/rpm/main.py index 0fd230aa1..5ab0930c6 100644 --- a/cachi2/core/package_managers/rpm/main.py +++ b/cachi2/core/package_managers/rpm/main.py @@ -47,6 +47,7 @@ class Package: vendor: Optional[str] = None checksum: Optional[str] = None repository_id: Optional[str] = None + summary: Optional[str] = None @classmethod def from_filepath(cls, rpm_filepath: Path, rpm_download_metadata: dict[str, Any]) -> "Package": @@ -85,6 +86,7 @@ def _query_rpm_fields(file_path: Path) -> dict[str, str]: "version=%{VERSION}\n" "release=%{RELEASE}\n" "arch=%{ARCH}\n" + "summary=%{SUMMARY}\n" # vendor and epoch are optional RPM tags; return "" if not set instead of "(None)" "vendor=%|VENDOR?{%{VENDOR}}:{}|\n" "epoch=%|EPOCH?{%{EPOCH}}:{}|\n" @@ -205,7 +207,14 @@ def fetch_rpm_source(request: Request) -> RequestOutput: for package in request.rpm_packages: path = request.source_dir.join_within_root(package.path) - components.extend(_resolve_rpm_project(path, request.output_dir, options=package.options)) + components.extend( + _resolve_rpm_project( + path, + request.output_dir, + options=package.options, + include_summary_in_sbom=package.include_summary_in_sbom, + ) + ) # FIXME: this is only ever good enough for a PoC, but needs to be handled properly in the # future. @@ -240,6 +249,7 @@ def _resolve_rpm_project( source_dir: RootedPath, output_dir: RootedPath, options: Optional[ExtraOptions] = None, + include_summary_in_sbom: bool = False, ) -> list[Component]: """ Process a request for a single RPM source directory. @@ -288,7 +298,7 @@ def _resolve_rpm_project( _verify_downloaded(metadata) lockfile_relative_path = source_dir.subpath_from_root / DEFAULT_LOCKFILE_NAME - return _generate_sbom_components(metadata, lockfile_relative_path) + return _generate_sbom_components(metadata, lockfile_relative_path, include_summary_in_sbom) def _download( @@ -383,14 +393,20 @@ def _is_rpm_file(file_path: Path) -> bool: def _generate_sbom_components( - files_metadata: dict[Path, Any], lockfile_path: Path + files_metadata: dict[Path, Any], + lockfile_path: Path, + include_summary_in_sbom: bool = False, ) -> list[Component]: components = [] for file_path, file_metadata in files_metadata.items(): if not _is_rpm_file(file_path): continue package = Package.from_filepath(file_path, file_metadata) - components.append(package.to_component(lockfile_path)) + component = package.to_component(lockfile_path) + if include_summary_in_sbom: + summary = Property(name="cachi2:rpm_summary", value=str(package.summary)) + component.properties.append(summary) + components.append(component) return components diff --git a/tests/integration/test_data/rpm_summary_is_reported/.build-config.yaml b/tests/integration/test_data/rpm_summary_is_reported/.build-config.yaml new file mode 100644 index 000000000..ecfb4f6a7 --- /dev/null +++ b/tests/integration/test_data/rpm_summary_is_reported/.build-config.yaml @@ -0,0 +1,2 @@ +environment_variables: [] +project_files: [] diff --git a/tests/integration/test_data/rpm_summary_is_reported/bom.json b/tests/integration/test_data/rpm_summary_is_reported/bom.json new file mode 100644 index 000000000..498182c97 --- /dev/null +++ b/tests/integration/test_data/rpm_summary_is_reported/bom.json @@ -0,0 +1,91 @@ +{ + "bomFormat": "CycloneDX", + "components": [ + { + "name": "glibc-common", + "properties": [ + { + "name": "cachi2:found_by", + "value": "cachi2" + }, + { + "name": "cachi2:rpm_summary", + "value": "Common binaries and locale data for glibc" + } + ], + "purl": "pkg:rpm/fedora/glibc-common@2.39-6.fc40?arch=x86_64&checksum=sha256:45fe79ffea9358fc7a4f233e2358b08678bdec476680d0655063b4a4058e8789&repository_id=releases", + "type": "library", + "version": "2.39" + }, + { + "name": "glibc-common", + "properties": [ + { + "name": "cachi2:found_by", + "value": "cachi2" + }, + { + "name": "cachi2:missing_hash:in_file", + "value": "another-project/rpms.lock.yaml" + } + ], + "purl": "pkg:rpm/fedora/glibc-common@2.39-6.fc40?arch=x86_64&repository_id=releases", + "type": "library", + "version": "2.39" + }, + { + "name": "glibc-minimal-langpack", + "properties": [ + { + "name": "cachi2:found_by", + "value": "cachi2" + } + ], + "purl": "pkg:rpm/fedora/glibc-minimal-langpack@2.38-7.fc39?arch=x86_64&checksum=sha256:6f9b45618d3b46fbbfb44407e05dd7054f3bd6a4df4f7291b576858b88deadc5&repository_id=releases", + "type": "library", + "version": "2.38" + }, + { + "name": "glibc-minimal-langpack", + "properties": [ + { + "name": "cachi2:found_by", + "value": "cachi2" + }, + { + "name": "cachi2:rpm_summary", + "value": "Minimal language packs for glibc." + } + ], + "purl": "pkg:rpm/fedora/glibc-minimal-langpack@2.39-6.fc40?arch=x86_64&checksum=sha256:9982f68ddcc3e972a2f3f220f29d56b0e9dde0e409bdecfe0bc559fe39013dde&repository_id=releases", + "type": "library", + "version": "2.39" + }, + { + "name": "gzip", + "properties": [ + { + "name": "cachi2:found_by", + "value": "cachi2" + }, + { + "name": "cachi2:rpm_summary", + "value": "GNU data compression program" + } + ], + "purl": "pkg:rpm/fedora/gzip@1.13-1.fc40?arch=x86_64&checksum=sha256:6dcc2f8885135fc873c8ab94a6c7df05883060c5b25287956bebb3aa15a84e71&repository_id=releases", + "type": "library", + "version": "1.13" + } + ], + "metadata": { + "tools": [ + { + "name": "cachi2", + "vendor": "red hat" + } + ] + }, + "specVersion": "1.4", + "version": 1 +} diff --git a/tests/integration/test_rpm.py b/tests/integration/test_rpm.py index 2b9a79af0..2eee48b3a 100644 --- a/tests/integration/test_rpm.py +++ b/tests/integration/test_rpm.py @@ -112,6 +112,22 @@ reason="CACHI2_TEST_LOCAL_DNF_SERVER!=true", ), ), + pytest.param( + utils.TestParameters( + repo="https://github.com/cachito-testing/cachi2-rpm", + ref="multiple-packages", + packages=( + {"path": "this-project", "type": "rpm", "include_summary_in_sbom": "true"}, + {"path": "another-project", "type": "rpm"}, + ), + flags=["--dev-package-managers"], + check_output=True, + check_deps_checksums=False, + check_vendor_checksums=False, + expected_exit_code=0, + ), + id="rpm_summary_is_reported", + ), ], ) def test_rpm_packages( diff --git a/tests/unit/models/test_input.py b/tests/unit/models/test_input.py index d0bd75d47..14b92a9b6 100644 --- a/tests/unit/models/test_input.py +++ b/tests/unit/models/test_input.py @@ -69,6 +69,7 @@ class TestPackageInput: "type": "rpm", "path": Path("."), "options": None, + "include_summary_in_sbom": False, }, ), ( @@ -80,6 +81,7 @@ class TestPackageInput: "foorepo": {"arch": "x86_64", "enabled": True}, } }, + "include_summary_in_sbom": False, }, { "type": "rpm", @@ -91,6 +93,7 @@ class TestPackageInput: }, "ssl": None, }, + "include_summary_in_sbom": False, }, ), ( @@ -110,6 +113,7 @@ class TestPackageInput: "ssl_verify": False, }, }, + "include_summary_in_sbom": False, }, ), ( @@ -138,6 +142,7 @@ class TestPackageInput: "ssl_verify": False, }, }, + "include_summary_in_sbom": False, }, ), ], diff --git a/tests/unit/package_managers/test_rpm.py b/tests/unit/package_managers/test_rpm.py index a2b325d17..1255c95a8 100644 --- a/tests/unit/package_managers/test_rpm.py +++ b/tests/unit/package_managers/test_rpm.py @@ -361,7 +361,7 @@ def test_resolve_rpm_project( mock_model_validate.return_value, mock_package_dir_path, None ) mock_verify_downloaded.assert_called_once_with({}) - mock_generate_sbom_components.assert_called_once_with({}, Path("rpms.lock.yaml")) + mock_generate_sbom_components.assert_called_once_with({}, Path("rpms.lock.yaml"), False) @mock.patch("cachi2.core.package_managers.rpm.main.run_cmd")