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.
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")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,
)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 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 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.
| Parameter | Default | Description |
|---|---|---|
name |
(required) | Rule name; also used as the output filename prefix (e.g. my_sbom → my_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. |
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 distroC++ dependency scanning (auto_cdxgen = True):
nvm install 20
npm install -g @cyclonedx/cdxgenSet auto_cdxgen = False if cdxgen is not available.
bazel build //:my_sbomGenerated in bazel-bin/:
my_sbom.spdx.json— SPDX 2.3my_sbom.cdx.json— CycloneDX 1.6my_sbom_crates_metadata.json— Rust crate cache (ifauto_crates_cache = True)my_sbom_cdxgen.cdx.json— C++ scan output (ifauto_cdxgen = True)
┌──────────────────┐
│ 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_depmodules - 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
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.
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.
Only transitive dependencies of the declared build targets are included. Build-time tools (compilers, build systems, test frameworks) are excluded via exclude_patterns.
- 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.
- 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 inhasExtractedLicensingInfosas required by SPDX 2.3; supplier is emitted asOrganization: <name>. - CycloneDX 1.6: Emitted with
"specVersion": "1.6"and"$schema": "https://cyclonedx.org/schema/bom-1.6.schema.json".
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
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.
| 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 |
Prerequisites for using these rules in your own project are covered in 3. Install Prerequisites. This section is about working on this repository itself.
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/bazelMODULE.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.lockCI fails if the lockfile is stale, so commit it in the same change.
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-fixThe license header text comes from the shared score_tooling template, so it is
identical to every other eclipse-score repository.