Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .bazelrc
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
common --registry=https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/
common --registry=https://bcr.bazel.build
common --lockfile_mode=error

build --java_language_version=17
build --tool_java_language_version=17
Expand Down
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
bazel-*
MODULE.bazel.lock

__pycache__
5 changes: 0 additions & 5 deletions BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,3 @@ filegroup(
],
)

# npm wrapper (uses system-installed npm from PATH)
sh_binary(
name = "npm_wrapper",
srcs = ["npm_wrapper.sh"],
)
13 changes: 13 additions & 0 deletions MODULE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ module(
)

bazel_dep(name = "rules_python", version = "1.4.1")
bazel_dep(name = "rules_nodejs", version = "6.7.3")

PYTHON_VERSION = "3.12"

Expand All @@ -27,6 +28,18 @@ python.toolchain(
python_version = PYTHON_VERSION,
)

uv = use_extension(
"@rules_python//python/uv:uv.bzl",
"uv",
dev_dependency = True,
)
uv.configure()
use_repo(uv, "uv")

node = use_extension("@rules_nodejs//nodejs:extensions.bzl", "node")
node.toolchain(node_version = "20.19.0")
use_repo(node, "nodejs", "nodejs_toolchains")

# score_tooling provides score_py_pytest test infrastructure (dev only)
bazel_dep(name = "score_tooling", dev_dependency = True)

Expand Down
1,147 changes: 1,147 additions & 0 deletions MODULE.bazel.lock

Large diffs are not rendered by default.

40 changes: 18 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,11 @@ 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. |
| `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). |
| `auto_crates_cache` | `True` | Runs `generate_crates_metadata_cache` at build time (requires network) to fetch Rust crate license, supplier, and description from crates.io API; 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. |
| `cdxgen_sbom` | `None` | Label to a pre-generated cdxgen CycloneDX JSON file; alternative to `auto_cdxgen` for C++ projects where automatic scanning is disabled or restricted in CI. Ignored for pure Rust projects. |
| `auto_cdxgen` | `False` | Runs cdxgen automatically inside the Bazel build using Bazel-managed Node/npm (no host `npm` or `nvm` installation required). Uses `no-sandbox` execution to scan the source tree. Ignored for pure Rust projects. |
| `cdxgen_version` | `"12.1.4"` | Pinned npm version for `@cyclonedx/cdxgen` used by `auto_cdxgen`; override to upgrade/downgrade deterministically. |
| `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. |
Expand All @@ -66,23 +67,20 @@ sbom(
| `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
## 3. Prerequisites

**Rust crate metadata** (`auto_crates_cache = True`):
No host-level installation of `nvm`, `npm`, `node`, or `openjdk` is required when using this SBOM rule as intended.

```bash
curl -LsSf https://astral.sh/uv/install.sh | sh
sudo apt install openjdk-11-jre-headless # or equivalent for your distro
```
- `auto_cdxgen = True`: cdxgen is executed via Bazel-managed tooling.
- `auto_crates_cache = True`: crate metadata cache generation is executed via Bazel-managed tooling.

**C++ dependency scanning** (`auto_cdxgen = True`):
If your environment blocks required network access, set `auto_cdxgen = False` and provide `cdxgen_sbom` explicitly.

```bash
nvm install 20
npm install -g @cyclonedx/cdxgen
```
### Open Point: Full Hermeticity

Set `auto_cdxgen = False` if cdxgen is not available.
`auto_cdxgen` is closer to hermetic now because it uses Bazel-managed Node/npm instead of host tooling.
It is not fully hermetic yet: `npm exec --package @cyclonedx/cdxgen@<version>` may still require network access during the build action.
Until offline package pinning/caching is introduced, air-gapped or fully reproducible pipelines should keep `auto_cdxgen = False` and pass a pre-generated `cdxgen_sbom`.

## 4. Build

Expand Down Expand Up @@ -115,9 +113,8 @@ Generated in `bazel-bin/`:
│ │ │
v v v
metadata.json _deps.json License + metadata
(module versions) (dep graph, (dash-license-scan
dep edges) + crates.io API
│ │ + cdxgen)
(module versions) (dep graph, (crates.io API
dep edges) + cdxgen)
└───────────────┼───────────────┘
v
Expand All @@ -134,13 +131,12 @@ Generated in `bazel-bin/`:
**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** — licenses data
- **crates.io API** — description and supplier for Rust crates
- **crates.io API** — licenses, descriptions, and suppliers 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), crates.io API (Rust), and cdxgen (C++). Cache files such as `cpp_metadata.json` must never be hand-edited.
All license, hash, supplier, and description values are derived from automated sources: `MODULE.bazel.lock`, `http_archive` rules, 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.

Expand Down Expand Up @@ -177,7 +173,7 @@ SHA-256 checksums come exclusively from `MODULE.bazel.lock` `registryFileHashes`

### 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.
- **Rust**: Licenses, 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. Optional `dash-license-scan` integration is still available in the cache generator for environments that require it.
- **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
Expand Down
3 changes: 3 additions & 0 deletions defs.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ def sbom(
dep_module_files = None,
cdxgen_sbom = None,
auto_cdxgen = False,
cdxgen_version = "12.1.4",
cargo_lockfile = None,
module_lockfiles = None,
auto_crates_cache = True,
Expand Down Expand Up @@ -64,6 +65,7 @@ def sbom(
dep_module_files: MODULE.bazel files from dependency modules for automatic version extraction
cdxgen_sbom: Optional label to CycloneDX JSON from cdxgen for C++ enrichment
auto_cdxgen: Run cdxgen automatically when no cdxgen_sbom is provided
cdxgen_version: Pinned @cyclonedx/cdxgen npm version for auto_cdxgen
cargo_lockfile: Optional Cargo.lock for crates metadata cache generation
module_lockfiles: MODULE.bazel.lock files for crate metadata extraction (e.g., from score_crates and workspace)
auto_crates_cache: Run crates metadata cache generation when cargo_lockfile or module_lockfiles is provided
Expand Down Expand Up @@ -119,6 +121,7 @@ def sbom(
dep_module_files = dep_module_files if dep_module_files else [],
cdxgen_sbom = cdxgen_sbom,
auto_cdxgen = auto_cdxgen,
cdxgen_version = cdxgen_version,
cargo_lockfile = cargo_lockfile,
module_lockfiles = module_lockfiles if module_lockfiles else [],
auto_crates_cache = auto_crates_cache,
Expand Down
50 changes: 31 additions & 19 deletions internal/rules.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -112,38 +112,45 @@ def _sbom_impl(ctx):
crates_cache = None
if (ctx.file.cargo_lockfile or ctx.files.module_lockfiles) and ctx.attr.auto_crates_cache:
crates_cache = ctx.actions.declare_file(ctx.attr.name + "_crates_metadata.json")
cache_inputs = [ctx.file._crates_cache_script]
cache_cmd = "set -euo pipefail\npython3 {} {}".format(
ctx.file._crates_cache_script.path,
crates_cache.path,
)
cache_inputs = []
cache_args = ctx.actions.args()
cache_args.add(crates_cache)
if ctx.file.cargo_lockfile:
cache_inputs.append(ctx.file.cargo_lockfile)
cache_cmd += " --cargo-lock {}".format(ctx.file.cargo_lockfile.path)
cache_args.add("--cargo-lock", ctx.file.cargo_lockfile)
for lock in ctx.files.module_lockfiles:
cache_inputs.append(lock)
cache_cmd += " --module-lock {}".format(lock.path)
ctx.actions.run_shell(
cache_args.add("--module-lock", lock)
ctx.actions.run(
inputs = cache_inputs,
outputs = [crates_cache],
command = cache_cmd,
executable = ctx.executable._crates_cache_script,
arguments = [cache_args],
mnemonic = "CratesCacheGenerate",
progress_message = "Generating crates metadata cache for %s" % ctx.attr.name,
execution_requirements = {"requires-network": ""},
use_default_shell_env = True,
)

# Add cdxgen SBOM if provided; otherwise auto-generate if enabled
cdxgen_sbom = ctx.file.cdxgen_sbom
if not cdxgen_sbom and ctx.attr.auto_cdxgen:
# Use the Bazel-managed Node.js toolchain for hermetic execution.
# node is the hermetic binary; npm_sources contains the npm JS entry point
# and all its supporting files declared by the toolchain.
node_info = ctx.toolchains["@rules_nodejs//nodejs:toolchain_type"].nodeinfo
cdxgen_sbom = ctx.actions.declare_file(ctx.attr.name + "_cdxgen.cdx.json")
ctx.actions.run(
outputs = [cdxgen_sbom],
executable = ctx.executable._npm,
tools = [node_info.node],
inputs = node_info.npm_sources,
executable = node_info.node,
arguments = [
node_info.npm.path,
"exec",
"--yes",
"--package=@cyclonedx/cdxgen@{}".format(ctx.attr.cdxgen_version),
"--",
"@cyclonedx/cdxgen",
"cdxgen",
"-t",
"cpp",
"--deep",
Expand All @@ -156,7 +163,11 @@ def _sbom_impl(ctx):
progress_message = "Generating cdxgen SBOM for %s" % ctx.attr.name,
# cdxgen needs to recursively scan source trees. Running sandboxed with
# only declared file inputs makes the scan effectively empty.
execution_requirements = {"no-sandbox": "1"},
# npm exec also resolves/downloads the pinned cdxgen package.
execution_requirements = {
"no-sandbox": "1",
"requires-network": "",
},
)

if cdxgen_sbom:
Expand Down Expand Up @@ -263,24 +274,25 @@ sbom_rule = rule(
default = False,
doc = "Automatically run cdxgen when no cdxgen_sbom is provided",
),
"_npm": attr.label(
default = "//:npm_wrapper",
executable = True,
cfg = "exec",
"cdxgen_version": attr.string(
default = "12.1.4",
doc = "Pinned @cyclonedx/cdxgen npm version used when auto_cdxgen is enabled",
),
"auto_crates_cache": attr.bool(
default = True,
doc = "Automatically build crates metadata cache when cargo_lockfile or module_lockfile is provided",
),
"_crates_cache_script": attr.label(
default = "//scripts:generate_crates_metadata_cache.py",
allow_single_file = True,
default = "//scripts:generate_crates_metadata_cache_bin",
executable = True,
cfg = "exec",
),
"_generator": attr.label(
default = "//internal/generator:sbom_generator",
executable = True,
cfg = "exec",
),
},
toolchains = ["@rules_nodejs//nodejs:toolchain_type"],
doc = "Generates SBOM for specified targets in SPDX and CycloneDX formats",
)
30 changes: 0 additions & 30 deletions npm_wrapper.sh

This file was deleted.

6 changes: 6 additions & 0 deletions scripts/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ py_library(
srcs = ["generate_crates_metadata_cache.py"],
)

py_binary(
name = "generate_crates_metadata_cache_bin",
srcs = ["generate_crates_metadata_cache.py"],
main = "generate_crates_metadata_cache.py",
)

py_library(
name = "generate_cpp_metadata_cache",
srcs = ["generate_cpp_metadata_cache.py"],
Expand Down
Loading