Use rules_nodejs toolchain for hermetic cdxgen execution - #4
Conversation
There was a problem hiding this comment.
Pull request overview
This PR aims to make SBOM generation more reproducible by moving cdxgen execution onto a Bazel-managed Node/npm toolchain and by refining how Rust crate metadata is generated (crates.io-first, with optional dash-license-scan).
Changes:
- Switched auto-cdxgen to run via
rules_nodejstoolchain and added a pinnedcdxgen_versionparameter. - Updated Rust crates metadata cache generation to use crates.io metadata by default, with optional dash-license-scan via CLI flag.
- Removed the legacy host
npm_wrapperand tightened Bazel reproducibility with lockfile enforcement.
Reviewed changes
Copilot reviewed 10 out of 12 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
internal/rules.bzl |
Uses Bazel-managed Node toolchain for cdxgen; refactors crates cache generation to a Bazel py_binary; adds cdxgen_version; adds Node toolchain requirement. |
defs.bzl |
Threads cdxgen_version through the public sbom() macro. |
scripts/generate_crates_metadata_cache.py |
Adds crates.io license fallback and optional dash-license-scan execution via --use-dash-license-scan. |
scripts/BUILD.bazel |
Adds py_binary target for generate_crates_metadata_cache to support Bazel execution. |
tests/test_generate_crates_metadata_cache.py |
Adds unit tests for crates.io license normalization and precedence over dash-license-scan. |
README.md |
Updates documentation to reflect Bazel-managed Node/npm usage, crates.io-first Rust metadata, and new cdxgen_version. |
MODULE.bazel |
Adds rules_nodejs and configures Node toolchain; configures uv extension as a dev dependency. |
MODULE.bazel.lock |
Adds the module lockfile to the repo to support --lockfile_mode=error reproducibility. |
BUILD.bazel |
Removes the legacy npm_wrapper sh_binary. |
npm_wrapper.sh |
Deletes the host npm/nvm wrapper script. |
.bazelrc |
Enforces lockfile reproducibility with --lockfile_mode=error. |
.gitignore |
Stops ignoring MODULE.bazel.lock so it can be checked in. |
Comments suppressed due to low confidence (1)
internal/rules.bzl:146
- The cdxgen action passes
node_info.npm.pathas a script argument, butnode_info.npmitself is not explicitly declared as an input/tool. Ifnpm_sourcesdoesn’t include the entrypoint file, the action will fail with a missing file at execution time.
ctx.actions.run(
outputs = [cdxgen_sbom],
tools = [node_info.node],
inputs = node_info.npm_sources,
executable = node_info.node,
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| ctx.actions.run( | ||
| inputs = cache_inputs, | ||
| outputs = [crates_cache], | ||
| command = cache_cmd, | ||
| executable = ctx.executable._crates_cache_script, | ||
| arguments = [cache_args], |
There was a problem hiding this comment.
Ran this end-to-end with auto_crates_cache = True in examples/dummy_project: the action executed fine and produced a correct _crates_metadata.json with valid SHA-256 checksums. Bazel wires up the py_binary's runfiles automatically when it's passed via ctx.executable.<attr> as executable=, so no separate tools= entry is needed here. Not an issue.
| cfg = "exec", | ||
| ), | ||
| }, | ||
| toolchains = ["@rules_nodejs//nodejs:toolchain_type"], |
There was a problem hiding this comment.
Fair point. Declaring toolchains = ["@rules_nodejs//nodejs:toolchain_type"] at the rule level does force Node toolchain resolution for every sbom_rule instantiation regardless of auto_cdxgen, unlike the old npm_wrapper.sh which only needed npm on the host if actually invoked. It's a deliberate trade-off for hermetic auto_cdxgen support, not a bug, but worth calling out for Rust-only/air-gapped consumers who never set auto_cdxgen = True.
|
@olivembo , is this PR still valid? Then please update and resolve conflicts, otherwise please close it |
|
Still valid. Rebased onto current |
node_info.npm is a self-contained shell wrapper (it locates its own node + npm-cli.js internally), not a JS entry point despite the NodeInfo docstring, so passing its path as an argument to node_info.node crashed with a JS syntax error. Run it directly as the executable instead. Also needs use_default_shell_env, without which npm's own "exec -- cdxgen" invocation could not find the cdxgen binary it had just resolved. Verified end-to-end by temporarily flipping auto_cdxgen/auto_crates_cache to True in examples/dummy_project and running a real build.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 11 out of 13 changed files in this pull request and generated 3 comments.
Suppressed comments (4)
README.md:143
- This blanket prerequisite is inaccurate for consumers that provide
python_lockfiles:auto_python_cacheremains enabled by default andgenerate_python_metadata_cache.pystill invokes hostuvxto run dash-license-scan, whose Java-based scanner requires a host Java runtime. Qualify this statement or update the Python cache path before claiming that no host Java installation is required.
No host-level installation of `nvm`, `npm`, `node`, or `openjdk` is required when using this SBOM rule as intended.
README.md:146
- This new prerequisite statement conflicts with the downstream smoke-test documentation:
examples/dummy_project/BUILD.bazel:29-34still saysauto_cdxgenrequires host npm and a globally installed cdxgen. Update that example comment (and any similar usage notes) so consumers are not told to install tooling that this change removed.
- `auto_cdxgen = True`: cdxgen is executed via Bazel-managed tooling.
- `auto_crates_cache = True`: crate metadata cache generation is executed via Bazel-managed tooling.
internal/rules.bzl:164
- The new action is not exercised by the repository's integration test:
examples/dummy_projectexplicitly setsauto_cdxgen = False, so CI does not validate the Node toolchain provider,npm execarguments, or the cdxgen output path introduced here. Add an opt-in integration test withauto_cdxgenenabled (or a hermetic toolchain stub) that verifies this execution path.
ctx.actions.run(
outputs = [cdxgen_sbom],
inputs = node_info.npm_sources,
executable = node_info.npm,
arguments = [
internal/rules.bzl:149
- This replacement drops the old action's
use_default_shell_env = Trueeven though the script still callsurllib.requestfor crates.io. Bazel actions do not inheritHTTP_PROXY/HTTPS_PROXY/NO_PROXYby default, so builds behind a required proxy can fail despiterequires-network. Preserve the needed proxy environment explicitly (or setuse_default_shell_env = True).
execution_requirements = {"requires-network": ""},
)
| dep_module_files = None, | ||
| cdxgen_sbom = None, | ||
| auto_cdxgen = False, | ||
| cdxgen_version = "12.1.4", |
There was a problem hiding this comment.
Good catch. Fixed in d732a15 by moving cdxgen_version after exclude_patterns, at the end of the optional parameter list, so pre-existing positional call sites keep binding correctly.
| ctx.actions.run( | ||
| outputs = [cdxgen_sbom], | ||
| executable = ctx.executable._npm, | ||
| inputs = node_info.npm_sources, |
There was a problem hiding this comment.
Confirmed this is real and pre-existing (not introduced by this diff — the prior implementation had no inputs at all for this action, so it had the same cache-invalidation gap). Documented the caveat inline as a known limitation rather than attempting a full fix here, since correctly keying this action would need collecting the transitive source-file list for the scanned target(s) — a bigger change than this PR's scope. Left as a follow-up.
| license_expr = _normalize_license_expression( | ||
| (crate.get("license") or "").strip() | ||
| ) |
There was a problem hiding this comment.
Actually worse than described. I checked the live crates.io API and the crate-level endpoint (/api/v1/crates/{name}) has no "license" field at all, so this fallback was returning "" for every crate, not just the wrong version's license. Fixed in d732a15 by fetching the version-scoped endpoint (/api/v1/crates/{name}/{version}) pinned to the locked version, plus a unit test asserting the locked version's license wins over a differently-licensed later release.
- generate_crates_metadata_cache.py: crates.io's crate-level endpoint has no license field at all (always ""), so the "crates.io license fallback" was a no-op. Fetch license from the version-scoped endpoint pinned to the locked version instead, so a crate that relicensed between the locked and latest version reports the correct one. - internal/rules.bzl: restore use_default_shell_env on the crates cache action, dropped when it was converted from run_shell to actions.run; without it, proxy env vars aren't inherited and requires-network builds can fail behind a required proxy. - defs.bzl: move the new cdxgen_version parameter to the end of sbom()'s optional parameter list so callers using the old positional argument order still bind correctly. - README.md / examples/dummy_project/BUILD.bazel: fix now-inaccurate prerequisite claims (python_lockfiles still needs a host JRE via dash-license-scan; the dummy_project comment still described the removed npm_wrapper.sh flow). - examples/dummy_project: add an opt-in, network-tagged smoke test (cdxgen_smoke_test) exercising auto_cdxgen=True end-to-end, since the default hermetic integration test always sets it False and so never caught the Node toolchain bugs fixed in the previous commit.
AlexanderLanin
left a comment
There was a problem hiding this comment.
sbom-tool is currently not truly maintained; no obvious problems in PR; merging PR as-is.
Summary
This PR makes SBOM generation more reproducible and closer to hermetic builds.
What changed
Updated docs/tests and removed legacy npm wrapper in README.md, test_generate_crates_metadata_cache.py, BUILD.bazel, and npm_wrapper.sh.
Notes
Includes both branch commits (23d543e, d56768c).
auto_cdxgen still requires network and no-sandbox behavior as configured.