Skip to content

Repository files navigation

About

SBOM tooling gives a set of bazel rules that generates a Software Bill of Materials in SPDX 2.3 and CycloneDX 1.6 format for a given Bazel target.

Setup

1. Configure MODULE.bazel

Add the SBOM metadata extension in your root MODULE.bazel:

sbom_ext = use_extension("@score_sbom//:extensions.bzl", "sbom_metadata")
use_repo(sbom_ext, "sbom_metadata")

For modules using local_path_override or git_override, also add a track_module tag for each such module. Without this, their versions cannot be auto-detected and will appear as unknown in the SBOM. The extension reads the version directly from the module's own MODULE.bazel file:

sbom_ext = use_extension("@score_sbom//:extensions.bzl", "sbom_metadata")
sbom_ext.track_module(name = "score_baselibs")
sbom_ext.track_module(name = "score_kyron")
use_repo(sbom_ext, "sbom_metadata")

2. Add SBOM Target in BUILD

load("@score_sbom//:defs.bzl", "sbom")

sbom(
    name = "my_sbom",
    targets = ["//my/app:binary"],
    component_name = "my_application",
    component_version = "1.0.0",
    module_lockfiles = [
        "@score_crates//:MODULE.bazel.lock",
        ":MODULE.bazel.lock",
    ],
    python_lockfiles = ["//path/to:requirements.txt.lock"],
    auto_crates_cache = True,
    auto_cdxgen = True,
)

SBOM modes

The rule supports two complementary SBOM modes. The modes are selected by the consumer's BUILD file; they are not separate Bazel rules.

Product mode

Product mode describes software delivered by the project. Pass the product or runtime binaries and libraries in targets, and enable the collectors needed for their shipped dependencies. Product SBOMs commonly emit both SPDX and CycloneDX:

sbom(
    name = "product_sbom",
    targets = ["//app:runtime_binary"],
    component_name = "my_product",
    output_formats = ["spdx", "cyclonedx"],
    module_lockfiles = [":MODULE.bazel.lock"],
    auto_cdxgen = True,
    auto_crates_cache = True,
)

Tool qualification mode

Tool mode describes software used to build, test, or generate documentation. It is kept separate from product mode because these components are not product runtime dependencies. Pass build-tool and documentation targets, use SPDX for the qualification inventory when required, and add the lockfiles and files used by those tools:

sbom(
    name = "build_tools_sbom",
    targets = [
      "//:docs",
      "//tools:build_tool",
      "@docs_as_code//:plantuml",
    ],
    component_name = "my_product_build_tools",
    output_formats = ["spdx"],
    auto_cdxgen = False,
    auto_crates_cache = False,
    python_lockfiles = [
      "//tools:requirements.txt.lock",
      "@docs_as_code//:requirements_lock",
    ],
    java_files = ["@docs_as_code//:plantuml.jar"],
    exclude_patterns = ["rules_python++pip+"],
)

The Python collector reads pinned pip-compile lockfiles and emits PyPI components with versions and SHA-256 hashes. The Java collector inventories declared .jar or other Java files as file-level components, including the file name, size, and SHA-256 checksum. This is useful for tools such as PlantUML, where the Python integration and the Java JAR are separate artifacts.

The host Java runtime itself (for example, the java executable installed by the operating system) is not a Bazel file input and therefore is not collected by java_files; it must be supplied through a separate host-tool inventory if the qualification scope requires the JDK installation as well.

Parameters

Parameter Default Description
name (required) Rule name; also used as the output filename prefix (e.g. my_sbommy_sbom.spdx.json).
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.
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.
java_files [] Java or JAR files to inventory as file-level components. Each file gets its name, size, and SHA-256 checksum; this does not inventory the host Java runtime.
auto_crates_cache True Runs generate_crates_metadata_cache at build time (requires network) to fetch Rust crate license and supplier data from dash-license-scan and crates.io; set to False only as a workaround for air-gapped or offline build environments — doing so produces a non-compliant SBOM where all Rust crates show NOASSERTION for license, supplier, and description. Has no effect when no lockfiles are provided (pure C++ projects).
cargo_lockfile None Path to a Cargo.lock file for crate enumeration; not needed when module_lockfiles is provided, as a synthetic Cargo.lock is generated from it automatically. Deprecated — will be removed in a future release.
cdxgen_sbom None Label to a pre-generated cdxgen CycloneDX JSON file; alternative to auto_cdxgen for C++ projects where cdxgen cannot run inside the Bazel build (e.g. CI environment without npm). Run cdxgen manually and pass its output here. Ignored for pure Rust projects.
auto_cdxgen False Runs cdxgen automatically inside the Bazel build (requires npm + @cyclonedx/cdxgen installed on the build machine); alternative to cdxgen_sbom for C++ projects. Uses no-sandbox execution to scan the source tree. Ignored for pure Rust projects.
output_formats ["spdx", "cyclonedx"] List of output formats to generate; valid values are "spdx" and "cyclonedx".
producer_name "Eclipse Foundation" Organisation name recorded as the SBOM producer.
producer_url Eclipse S-CORE URL URL of the SBOM producer organisation. When this is a https://github.com/<owner> URL, the <owner> segment is also used as the GitHub namespace in the root component's pkg:github/… PURL. For non-GitHub URLs the PURL namespace falls back to eclipse-score.
sbom_authors None List of author strings written into SBOM metadata; defaults to producer_name when omitted.
namespace https://eclipse.dev/score URI used as the SPDX document namespace and CycloneDX serial number base.
generation_context None CycloneDX lifecycle phase label (e.g. "build", "release").
sbom_tools None List of tool name strings recorded in SBOM metadata alongside the generator itself.
exclude_patterns None List of repo name substrings to exclude from the dependency graph (e.g. build tools, test frameworks).
dep_module_files None MODULE.bazel files from dependency modules used for additional automatic version extraction.
metadata_json @sbom_metadata//:metadata.json Label to the metadata JSON produced by the sbom_metadata Bazel extension; rarely needs changing.

3. Install Prerequisites

These are needed on the build machine of a project that uses these rules. To work on this repository itself, see Development instead.

Rust crate metadata (auto_crates_cache = True):

curl -LsSf https://astral.sh/uv/install.sh | sh
sudo apt install openjdk-11-jre-headless  # or equivalent for your distro

C++ dependency scanning (auto_cdxgen = True):

nvm install 20
npm install -g @cyclonedx/cdxgen

Set auto_cdxgen = False if cdxgen is not available.

4. Build

bazel build //:my_sbom

5. Output

Generated in bazel-bin/:

  • my_sbom.spdx.json — SPDX 2.3
  • my_sbom.cdx.json — CycloneDX 1.6
  • my_sbom_crates_metadata.json — Rust crate cache (if auto_crates_cache = True)
  • my_sbom_cdxgen.cdx.json — C++ scan output (if auto_cdxgen = True)

Architecture

                        ┌──────────────────┐
                        │   Bazel build    │
                        └────────┬─────────┘
                                 │
                 ┌───────────────┼───────────────┐
                 │               │               │
                 v               v               v
          MODULE.bazel     Bazel targets    Lockfiles
                 │               │               │
                 v               v               v
          metadata.json    _deps.json      License + metadata
        (module versions)  (dep graph,     (dash-license-scan
                          dep edges)      + crates.io API
                 │               │           + cdxgen)
                 └───────────────┼───────────────┘
                                 │
                                 v
                        ┌──────────────────┐
                        │ sbom_generator   │
                        │ (match & resolve)│
                        └────────┬─────────┘
                                 │
                        ┌────────┴────────┐
                        v                 v
                   .spdx.json        .cdx.json

Data sources:

  • Bazel module graph — version, PURL, and registry info for bazel_dep modules
  • Bazel aspect — transitive dependency graph and external repo dependency edges
  • dash-license-scan — Rust and Python license data from the Eclipse Foundation and ClearlyDefined services
  • crates.io API — description and supplier for Rust crates
  • cdxgen — C++ dependency licenses, descriptions, and suppliers

Automated Metadata Sources

All license, hash, supplier, and description values are derived from automated sources: MODULE.bazel.lock, http_archive rules, dash-license-scan (Rust and Python), crates.io API (Rust), and cdxgen (C++). Cache files such as cpp_metadata.json must never be hand-edited.

CPE, aliases, and pedigree are the only fields that may be set manually via sbom_ext.license(), as they represent identity and provenance annotations that cannot be auto-deduced.

Required SBOM Fields (CISA 2025)

Every component entry in the generated SBOM must include the following fields, as mandated by CISA 2025 minimum elements:

Field SPDX 2.3 CycloneDX 1.6 Source Description
Component name name components[].name Extracted Human-readable name of the dependency (e.g. serde, boost.mp11).
Component version versionInfo components[].version Extracted Exact released version string used in the build.
Component hash (SHA-256) checksums[SHA256] components[].hashes Extracted SHA-256 digest of the downloaded archive, taken exclusively from registryFileHashes in MODULE.bazel.lock (BCR modules) or the sha256 field of http_archive rules; omitted when neither provides one, rather than emitting an incorrect value.
Software identifier (PURL) externalRefs[purl] components[].purl Extracted Package URL uniquely identifying the component by ecosystem, name, and version (e.g. pkg:cargo/serde@1.0.228).
License expression licenseConcluded components[].licenses Extracted SPDX license expression concluded for this component (e.g. Apache-2.0 OR MIT).
Dependency relationships relationships[DEPENDS_ON] dependencies Extracted Graph edges recording which component depends on which, enabling consumers to reason about transitive exposure.
Supplier supplier components[].supplier.name Extracted Organisation or individual that distributes the component (e.g. the crates.io publisher name).
Component description description components[].description Extracted Short human-readable summary of what the component does; set to "Missing" when no source can provide it.
SBOM author creationInfo.creators metadata.authors Configured Entity responsible for producing this SBOM document; set via producer_name in the sbom() rule (default: Eclipse Foundation).
Tool name creationInfo.creators metadata.tools Auto-generated Name and version of the tool that generated the SBOM.
Timestamp creationInfo.created metadata.timestamp Auto-generated ISO-8601 UTC timestamp recording when the SBOM was generated.
Generation context (lifecycle) metadata.lifecycles Auto-generated CycloneDX lifecycle phase at which the SBOM was produced (e.g. build).

Legend: Extracted — derived automatically from the Bazel dependency graph, lockfiles, or external registries (crates.io, cdxgen). Configured — comes from an sbom() rule parameter with a sensible default. Auto-generated — computed at build time with no user input required.

Fields are populated automatically from the sources described in Automated Metadata Sources and License Data by Language. If a source cannot provide a value (e.g. cdxgen cannot resolve a C++ component), the field is omitted rather than filled with incorrect data — except for description, which is set to "Missing" to make the gap visible.

Component Scope

Only transitive dependencies of the declared build targets are included. Build-time tools (compilers, build systems, test frameworks) are excluded via exclude_patterns.

License Data by Language

  • Rust: Licenses via dash-license-scan (Eclipse Foundation + ClearlyDefined); descriptions and suppliers from crates.io API. Crates with platform-specific suffixes (e.g. iceoryx2-bb-lock-free-qnx8) fall back to the base crate name for lookup.
  • Python: Licenses via dash-license-scan (Eclipse Foundation + ClearlyDefined), using pypi/pypi/-/<name>/<version> identifiers generated from pip-compile lockfiles. Descriptions and suppliers are not enriched yet.
  • C++: Licenses, descriptions, and suppliers via cdxgen source tree scan. There is no dash-license-scan integration for C++ — it does not support pkg:generic/... PURLs used by BCR modules. If cdxgen cannot resolve a component, its description is set to "Missing" and its license field is empty.

Output Format Versions

  • SPDX 2.3: Migration to SPDX 3.0 is deferred until it is supported in production by at least one major consumer (Trivy, GitHub Dependabot, or Grype) and the reference Python library no longer marks its own 3.0 support as experimental. Neither condition was met when this was last reviewed. LicenseRef-* identifiers are declared in hasExtractedLicensingInfos as required by SPDX 2.3; supplier is emitted as Organization: <name>.
  • CycloneDX 1.6: Emitted with "specVersion": "1.6" and "$schema": "https://cyclonedx.org/schema/bom-1.6.schema.json".

How design is tested

Two layers: unit tests over the generator internals, and an end-to-end integration test that builds a real project through the public sbom() macro.

# From sbom-tool/ — unit tests, against captured fixtures
bazel test //tests/...

# From sbom-tool/examples/dummy_project/ — end-to-end, against a real build
bazel test //...

Sbom was also tested by external tool https://sbomgenerator.com/tools/validator

End-to-end integration test

examples/dummy_project is a small C++ and Rust project that consumes these rules the way a downstream repository does: it declares bazel_dep(name = "score_sbom") (resolved through local_path_override to the checkout), calls sbom() on a cc_binary and a rust_binary, and asserts the resulting SPDX and CycloneDX documents in sbom_test.py.

It is a separate Bazel module, listed in the repository root .bazelignore, so it must be built from its own directory and never leaks its dependencies (nlohmann_json, rules_rust, score_crates) into consumers of @score_sbom. Its MODULE.bazel.lock is tracked in git for the same reason the root one is, plus one of its own: the sbom() target consumes the lockfile as a build input, so a checkout without it fails to build.

auto_cdxgen and auto_crates_cache are off, so the test needs no npm and no calls to dash-license-scan or crates.io during SBOM generation. A cold build still fetches bzlmod modules and toolchains from the configured registries like any other Bazel build. The test covers the aspect, the rule and the module extension — the parts no fixture can cover — rather than full license enrichment.

Tests description

Test file Bazel target What it covers
test_bcr_known_licenses.py test_bcr_known_licenses BCR_KNOWN_LICENSES table; apply_known_licenses() priority chain (5 levels); resolve_component() integration after license resolution
test_cpp_enrich_checksum.py test_cpp_enrich_checksum enrich_components_from_cpp_cache() field propagation (checksum, normalised names, parent match); no-manual-curation rule on cpp_metadata.json
test_cyclonedx_formatter.py test_cyclonedx_formatter CycloneDX 1.6 document structure; license encoding (single id vs compound expression); or/and normalisation; dependency graph; _normalize_spdx_license()
test_spdx_formatter.py test_spdx_formatter SPDX 2.3 document structure; PURL as externalRef; SHA-256 checksums; DESCRIBES/DEPENDS_ON relationships; hasExtractedLicensingInfos for LicenseRef-*; _normalize_spdx_license()
test_sbom_generator.py test_sbom_generator filter_repos(); resolve_component() (all 8 repo-type branches); deduplicate_components(); parse_module_bazel_files(); parse_module_lockfiles(); mark_missing_cpp_descriptions(); main() end-to-end (15 scenarios: SPDX/CycloneDX output, BCR licenses, crate_universe, exclude patterns, version auto-detect, dep_module_files, module_lockfiles, --crates-cache, --cdxgen-sbom, output file selection)
test_generate_crates_metadata_cache.py test_generate_crates_metadata_cache parse_dash_summary(); parse_module_bazel_lock(); generate_synthetic_cargo_lock(); end-to-end summary CSV round-trip
test_generate_cpp_metadata_cache.py test_generate_cpp_metadata_cache convert_cdxgen_to_cache(): version, license (id/name/expression/AND), supplier (name/publisher fallback), PURL, URL from externalReferences, description
test_spdx_to_github_snapshot.py test_spdx_to_github_snapshot convert_spdx_to_snapshot(): top-level fields; direct vs. indirect classification; package filtering; manifest naming; pkg:generic/ PURL support
test_real_sbom_integration.py test_real_sbom_integration End-to-end generation from captured reference_integration fixtures (baselibs, kyron, orchestrator): SPDX and CycloneDX validity; package counts; crate license enrichment and checksums; module version enrichment from the lockfile; LicenseRef-* declarations; bom-ref uniqueness; every SPDXID resolving to a real node
examples/dummy_project/sbom_test.py //:sbom_test (in examples/dummy_project) The SBOM of an actual Bazel build: SPDX 2.3 and CycloneDX 1.6 headers, root component identity, one C++ (nlohmann_json) and one Rust (serde) dependency proving both toolchain paths were traversed, license fields on every package, no dangling SPDXID or bom-ref, and the two formats agreeing on the component set

Development

Prerequisites for using these rules in your own project are covered in 3. Install Prerequisites. This section is about working on this repository itself.

Prerequisites

The Bazel version is pinned in .bazelversion, so contributors need bazelisk rather than a distribution package. A distro bazel reads the pin but only looks for that version under /usr/bin, and fails with requires Bazel <version> ... but it wasn't found:

curl -Lo ~/.local/bin/bazel \
  https://github.com/bazelbuild/bazelisk/releases/latest/download/bazelisk-linux-amd64
chmod +x ~/.local/bin/bazel

MODULE.bazel.lock is tracked in git: it pins the exact resolved dependency graph, which matters here because .bazelrc resolves against the mutable main branch of the score registry. After changing MODULE.bazel, regenerate it:

bazel mod tidy
bazel mod deps
git add MODULE.bazel MODULE.bazel.lock

CI fails if the lockfile is stale, so commit it in the same change.

Continuous integration

Every pull request runs:

Workflow Job What it does
tests.yml unit_tests bazel test //tests/...
tests.yml integration_test bazel test //... in examples/dummy_project — builds the example and checks its SBOM
format.yml formatting-check //tools:format.check (ruff format, buildifier, yamlfmt) via eclipse-score/cicd-workflows
copyright.yml copyright-check //tools:copyright-check via eclipse-score/cicd-workflows
bzlmod-lock.yml bzlmod-lock bazel mod tidy + bazel mod deps --lockfile_mode=error via eclipse-score/cicd-workflows

The formatting job checks layout only — it is not a linter. Unused imports, undefined names and similar are not gated.

Fix formatting and license headers locally with:

bazel run //tools:format.fix
bazel run //tools:copyright-fix

The license header text comes from the shared score_tooling template, so it is identical to every other eclipse-score repository.


About

Home of the SBOM generation tool

Resources

Security policy

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages