From 3c69be154404a333e9a91608725684040fe89067 Mon Sep 17 00:00:00 2001 From: Uwe Maucher <205777204+umaucher@users.noreply.github.com> Date: Wed, 23 Sep 2026 16:52:39 +0200 Subject: [PATCH 1/2] Add SPDX license metadata to generated SBOM packages Ensure component license declarations reach SPDX output, and default SCORE dependencies to Apache-2.0 when no license metadata is available. --- defs.bzl | 3 +++ internal/generator/spdx_formatter.py | 17 ++++++++++++++--- internal/rules.bzl | 5 +++++ tests/test_spdx_formatter.py | 25 +++++++++++++++++++++++++ 4 files changed, 47 insertions(+), 3 deletions(-) diff --git a/defs.bzl b/defs.bzl index 1921a94..deaa8f1 100644 --- a/defs.bzl +++ b/defs.bzl @@ -50,6 +50,7 @@ def sbom( producer_url = "https://projects.eclipse.org/projects/automotive.score", component_name = None, component_version = None, + component_license = None, sbom_authors = None, generation_context = None, sbom_tools = None, @@ -98,6 +99,7 @@ def sbom( PURL namespace falls back to ``eclipse-score``. component_name: Main component name (defaults to rule name) component_version: Component version string + component_license: SPDX license for the main component namespace: SBOM namespace URI (defaults to https://eclipse.dev/score) exclude_patterns: Repo patterns to exclude (e.g., build tools) **kwargs: Additional arguments passed to the underlying rule @@ -164,6 +166,7 @@ def sbom( producer_url = producer_url, component_name = component_name if component_name else name, component_version = component_version if component_version else "", + component_license = component_license if component_license else "", sbom_authors = sbom_authors if sbom_authors else [], generation_context = generation_context if generation_context else "", sbom_tools = sbom_tools if sbom_tools else [], diff --git a/internal/generator/spdx_formatter.py b/internal/generator/spdx_formatter.py index c270ee0..0c4f7b3 100644 --- a/internal/generator/spdx_formatter.py +++ b/internal/generator/spdx_formatter.py @@ -46,6 +46,9 @@ def generate_spdx( component_name = config.get("component_name", "unknown") component_version = config.get("component_version", "") producer_name = config.get("producer_name", "Eclipse Foundation") + component_license = _default_score_license( + component_name, config.get("component_license", "") + ) doc_uuid = uuid.uuid4() @@ -62,8 +65,8 @@ def generate_spdx( "supplier": f"Organization: {producer_name}", "primaryPackagePurpose": "APPLICATION", "filesAnalyzed": False, - "licenseConcluded": "NOASSERTION", - "licenseDeclared": "NOASSERTION", + "licenseConcluded": component_license or "NOASSERTION", + "licenseDeclared": component_license or "NOASSERTION", "copyrightText": "NOASSERTION", } packages.append(root_package) @@ -131,7 +134,7 @@ def _create_spdx_package( name = component.get("name", "unknown") version = component.get("version", "unknown") purl = component.get("purl", "") - license_id = _normalize_spdx_license(component.get("license", "")) + license_id = _default_score_license(name, component.get("license", "")) description = component.get("description", "") supplier = component.get("supplier", "") comp_type = component.get("type", "library") @@ -185,6 +188,14 @@ def _create_spdx_package( return package, spdx_id +def _default_score_license(name: str, license_id: str) -> str: + """Use the SCORE default license when a score module has no metadata.""" + normalized = _normalize_spdx_license(license_id) + if not normalized and name.startswith("score_"): + return "Apache-2.0" + return normalized + + def _map_type_to_purpose(comp_type: str) -> str: """Map component type to SPDX 2.3 primary package purpose.""" type_mapping = { diff --git a/internal/rules.bzl b/internal/rules.bzl index 017b3a2..3c5afd4 100644 --- a/internal/rules.bzl +++ b/internal/rules.bzl @@ -94,6 +94,7 @@ def _sbom_impl(ctx): "producer_url": ctx.attr.producer_url, "component_name": ctx.attr.component_name if ctx.attr.component_name else ctx.attr.name, "component_version": ctx.attr.component_version, + "component_license": ctx.attr.component_license, "namespace": ctx.attr.namespace, "sbom_authors": ctx.attr.sbom_authors, "generation_context": ctx.attr.generation_context, @@ -258,6 +259,10 @@ sbom_rule = rule( default = "", doc = "Component version", ), + "component_license": attr.string( + default = "", + doc = "SPDX license for the main component", + ), "sbom_authors": attr.string_list( default = [], doc = "SBOM author(s) (distinct from software producers)", diff --git a/tests/test_spdx_formatter.py b/tests/test_spdx_formatter.py index 6f33522..8889659 100644 --- a/tests/test_spdx_formatter.py +++ b/tests/test_spdx_formatter.py @@ -141,6 +141,31 @@ def test_generate_spdx_with_empty_components(self): # Only root package self.assertEqual(len(packages), 1) + def test_score_packages_default_to_apache_license(self): + """SCORE packages without license metadata default to Apache-2.0.""" + config = {**self.config, "component_name": "score_persistency"} + components = [{"name": "score_baselibs", "version": "0.2.13"}] + + spdx = generate_spdx(components, config, self.timestamp) + + root_package, score_package = spdx["packages"] + self.assertEqual(root_package["licenseDeclared"], "Apache-2.0") + self.assertEqual(score_package["licenseDeclared"], "Apache-2.0") + + def test_component_license_overrides_score_default(self): + """Explicit license metadata remains authoritative for SCORE packages.""" + components = [ + { + "name": "score_baselibs", + "version": "0.2.13", + "license": "MIT", + } + ] + + spdx = generate_spdx(components, self.config, self.timestamp) + + self.assertEqual(spdx["packages"][1]["licenseDeclared"], "MIT") + def test_generate_spdx_component_purl(self): """Test that component PURLs are properly set.""" spdx = generate_spdx(self.components, self.config, self.timestamp) From 2ebe5d0f02d2d10c5e160adf5758ea92df785248 Mon Sep 17 00:00:00 2001 From: Uwe Maucher <205777204+umaucher@users.noreply.github.com> Date: Fri, 25 Sep 2026 09:24:15 +0200 Subject: [PATCH 2/2] test: cover root component license propagation --- README.md | 1 + tests/test_spdx_formatter.py | 14 ++++++++++++++ 2 files changed, 15 insertions(+) diff --git a/README.md b/README.md index a4bc98f..e7e9f44 100644 --- a/README.md +++ b/README.md @@ -115,6 +115,7 @@ the qualification scope requires the JDK installation as well. | `targets` | *(required)* | Bazel targets whose transitive dependencies are included in the SBOM. | | `component_name` | rule `name` | Name of the root component written into the SBOM; defaults to the rule name if omitted. | | `component_version` | `None` | Version string for the root component; auto-detected from the module graph when omitted. | +| `component_license` | `None` | SPDX license expression for the root component; SCORE components without explicit license metadata default to `Apache-2.0`. | | `module_lockfiles` | `[]` | One or more `MODULE.bazel.lock` files used to extract dependency versions and SHA-256 checksums; C++ projects need only the workspace lockfile (`:MODULE.bazel.lock`), Rust projects should also pass `@score_crates//:MODULE.bazel.lock` to cover crate versions and checksums. | | `python_lockfiles` | `[]` | One or more pip-compile lockfiles (`requirements.txt.lock`) used to add pinned PyPI packages, SHA-256 hashes, and license expressions from DASH. Packages that DASH cannot verify retain `NOASSERTION`; descriptions are not enriched yet and remain `Missing`. | | `auto_python_cache` | `True` | Generates Python package metadata from `python_lockfiles`; set to `False` to disable it. | diff --git a/tests/test_spdx_formatter.py b/tests/test_spdx_formatter.py index 8889659..445392b 100644 --- a/tests/test_spdx_formatter.py +++ b/tests/test_spdx_formatter.py @@ -166,6 +166,20 @@ def test_component_license_overrides_score_default(self): self.assertEqual(spdx["packages"][1]["licenseDeclared"], "MIT") + def test_root_component_license_overrides_score_default(self): + """Explicit root license metadata remains authoritative for SCORE packages.""" + config = { + **self.config, + "component_name": "score_persistency", + "component_license": "MIT OR Apache-2.0", + } + + spdx = generate_spdx([], config, self.timestamp) + + root_package = spdx["packages"][0] + self.assertEqual(root_package["licenseDeclared"], "MIT OR Apache-2.0") + self.assertEqual(root_package["licenseConcluded"], "MIT OR Apache-2.0") + def test_generate_spdx_component_purl(self): """Test that component PURLs are properly set.""" spdx = generate_spdx(self.components, self.config, self.timestamp)