Skip to content

tools,build: add CycloneDX SBOM generation - #66761

Draft
BethGriggs wants to merge 4 commits into
nodejs:mainfrom
BethGriggs:poc-sbom-impl
Draft

BethGriggs wants to merge 4 commits into
nodejs:mainfrom
BethGriggs:poc-sbom-impl

Conversation

@BethGriggs

@BethGriggs BethGriggs commented Oct 15, 2026 •

Copy link
Copy Markdown
Member

This is a proof-of-concept for build-time SBOM generation in Node.js core (nodejs/security-wg#1115).

The PR adds tools/generate-sbom.py, a stdlib-only Python 3 script that produces a CycloneDX 1.7 JSON Software Bill of Materials covering all bundled runtime dependencies of the Node.js binary.

The script reads config.gypi (produced by ./configure) to determine which dependencies are active in the current build, then extracts version strings from committed C headers, CMakeLists.txt files, package.json files,
and other metadata sources across all deps/ subdirectories.

./configure
make sbom       # → out/Release/node-v27.0.0-sbom.json
make test-sbom  # validates the output

Pre-emptive questions

  • How much ongoing maintenance does this require?
    • Little. Dep version bumps are picked up automatically from committed headers and package.json files. New JS files in lib/ flow through config.gypi automatically. The only manual step is one DEP_CATALOG entry when a new C/C++ dep is added - a CI test enforces this so a missing entry fails rather than producing a silent gap.
  • Why CycloneDX and not SPDX?:
    • npm already ships a CycloneDX 1.5 implementation in deps/npm/lib/utils/sbom-cyclonedx.js. SPDX support could be added later behind a --format flag.
  • Why Python and not a Node.js script?:
    • The script runs before node is built. ./configure just needs to have run. Python is already the language of the entire build toolchain (configure.py, gyp_node.py, install.py).
  • Why not generate this automatically during make build / make release?
    • Intentionally out of scope for this PR, the idea is that we'd review and iterate before committing to ship it as a release asset.

SBOM snippets

{
  "$schema": "http://cyclonedx.org/schema/bom-1.7.schema.json",
  "bomFormat": "CycloneDX",
  "specVersion": "1.7",
  "serialNumber": "urn:uuid:17555426-32bc-454a-b766-1a7de082741c",
  "version": 1,
  "metadata": {
    "timestamp": "2026-10-15T13:21:37.952127+00:00",
    "lifecycles": [
      {
        "phase": "build"
      }
    ],
...
    "component": {
      "bom-ref": "nodejs@27.0.0",
      "type": "framework",
      "name": "nodejs",
      "version": "27.0.0",
      "description": "Node.js JavaScript runtime",
      "purl": "pkg:github/nodejs/node@27.0.0"
    },

 ...
 "components": [
    {
      "bom-ref": "openssl@3.5.9",
      "type": "library",
      "name": "openssl",
      "version": "3.5.9",
      "description": "TLS/SSL and general-purpose cryptography library",
      "scope": "required",
      "purl": "pkg:github/openssl/openssl@3.5.9"
    },
    {
      "bom-ref": "zlib@1.3.2.1",
      "type": "library",
      "name": "zlib",
      "version": "1.3.2.1",
      "description": "General-purpose lossless data compression library",
      "scope": "required",
      "purl": "pkg:github/madler/zlib@1.3.2.1"
    },
    ...

Add tools/generate_sbom.py, a stdlib-only Python 3 script that writes a
CycloneDX 1.5 SBOM describing the bundled dependencies of the Node.js
binary.

The script reads config.gypi to determine which dependencies are active
in the current build, then extracts version strings from whichever file
each dependency uses to record its version: C headers, CMakeLists.txt,
package.json, Cargo.toml and others. A dependency whose version cannot
be read is reported as "unknown" with a warning rather than failing the
run. Dependencies satisfied by a system library or disabled via a
configure flag are emitted with scope "excluded", so the document
reflects the build that was configured.

Output defaults to out/Release/node-v{version}-sbom.json.

New Makefile targets:

  make sbom       - generate the SBOM (requires ./configure first)
  make test-sbom  - validate the generated SBOM

doc/contributing/maintaining/maintaining-dependencies.md now asks for a
DEP_CATALOG entry when adding a dependency, so new deps do not silently
go missing from the SBOM.

Unit tests in test/tools/test_generate_sbom.py cover catalog structural
correctness, the version extractors, scope resolution and deps/
coverage. They run under the existing `make tooltest` target.

Refs: nodejs/security-wg#1115

Assisted-by: IBM Bob
Signed-off-by: Beth Griggs <bethanyngriggs@gmail.com>
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Review requested:

  • @nodejs/tsc

@nodejs-github-bot nodejs-github-bot added build Issues and PRs related to Node.js builds or CI infrastructure. needs-ci PRs that need a full CI run. tools Issues and PRs related to the tools directory. labels Oct 15, 2026
@marco-ippolito

Copy link
Copy Markdown
Member

Last time I gave it a try I was having an hard time because cyclondx crashed trying to analyze the vendored deps. Glad to see this issue has been fixed

@richardlau richardlau left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is good start. It doesn't look like much to maintain (a large part appears to be the catalog).

Maybe in the future we can deduplicate version discovery between the deps_updaters, scanning in https://github.com/nodejs/nodejs-dependency-vuln-assessments and this tool, but certainly doesn't have to happen in the first pass.

Comment thread tools/generate_sbom.py Outdated
'version_fn': lambda root: version_from_readme_node(
root / 'deps/inspector_protocol/README.node', 'Revision'),
},
# --- Always-bundled ICU, JavaScript and Rust dependencies ---

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Most of these are not always bundled.
e.g.

  • icu could be omitted with configure --with-intl=none
  • npm could be omitted with configure --without-npm
  • amaro could be omitted with configure --without-amaro
  • Rust dependencies could be omitted if configure --v8-disable-temporal-support, or no Rust compiler present

Corepack is no longer bundled by default (but can be included with configure --with-corepack).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops, the comment was just incorrect 🤦🏻‍♀️ . I've tried to take into account the various optionals in 7221ccd. They will now not be included in the SBOM when omitted.

I do still need to check all options are covered and how we keep them up-to-date - I'll explore if I can add test cases to help with that.

Previously, `generate_sbom.py` emitted components for dependencies that
were not linked into the binary, marking them with the `excluded` scope.
Drop those components so the SBOM only lists what ships in the build.

Add `excluded_when` conditions to catalog entries whose inclusion
depends on configure flags:

* perfetto: only included with `--with-perfetto`
* inspector_protocol: omitted when the inspector is disabled
* icu: omitted with `--with-intl=none`
* npm: omitted with `--without-npm`
* corepack: only included with `--with-corepack`
* node_crates: omitted when Temporal support is disabled or when
  `--shared-temporal_capi` is used

Mark icu, corepack and node_crates as `optional` since they are not
always bundled. Skip components that resolve to `excluded` in
`generate_sbom()` and drop the now-redundant `excluded` filtering when
building the root component's `dependsOn` list and when warning about
unreadable versions.

Signed-off-by: Beth Griggs <bethanyngriggs@gmail.com>
Assisted-by: IBM Bob
@BethGriggs

Copy link
Copy Markdown
Member Author

@marco-ippolito, ah, this is taking a different approach and doesn't use any SBOM tooling. The idea is that we inspect the config.gypi to determine what top-level deps (and their versions) are included in the build. The output is just written to conform to the CycloneDX spec.

@pombredanne

Copy link
Copy Markdown

@BethGriggs two quick notes:

  • CycloneDX 1.5 is kinda old... I'd suggest going at least 1.6 or 1.7, though this is likely minor
  • We are adding PURL support for unincorporated packages with the upcoming "software-id" type, and the ASF and the kernel are already in to use it for their projects. This is a few weeks out.

(Disclosure: I maintain PURL and contribute to CDX core)

@marco-ippolito

marco-ippolito commented Oct 18, 2026 •

Copy link
Copy Markdown
Member

On a note the purl used by most tooling is generic/node, however we discussed (nodejs/security-wg#1552) that the right purl would be with the namespace generic/nodejs/node so I guess its good to know

Signed-off-by: Beth Griggs <bethanyngriggs@gmail.com>
Signed-off-by: Beth Griggs <bethanyngriggs@gmail.com>
@sxa

sxa commented Oct 20, 2026

Copy link
Copy Markdown
Member

Looks great as a first pass! In the current climate I think we really need this so thank you for pushing it forward.

Some thoughts on a few of things that I think we should look at including in this if we can in the initial drop if practical as a few of these should be pretty trivial to add for most of the UNIX-based platforms. I'm listing these in order of what I think should be highest to lowest priority:

  1. Versions of the compilers (C/C++/rust) used since that's an important dependency that is important for the supply chain as it directly affects what goes into the build ($CC --version | head -1 if $CC is set) which is typically will be in the CI for UNIX-based platforms). Going forward we should also consider how to record the precise package versions of the compiler and things like the glibc headers that are being used as they are dependencies that get included in the product.
  2. The git sha that is being built from (git rev-parse HEAD output). While we record the node.js version which should lead someone to the source tag the SHA is preferable as it's immutable. In situations where we carry floating patches on top of any in-tree components which are already listed that would allow the exact code that went into the build to be identified by the SBoM consumer (and again allow that information to be available for reproducibility purposes). On that topic we should perhaps include the v8_embedder_string which can help identify the set of patches that we've applied there.
  3. The "builder" name (typically the output from hostname would be enough although the jenkins ) that the build is done on, in part because in our case that typically provides an easy way to identify the operating system that was used to build it (except for our macos/orka systems where they are ephemeral machines without a descriptive name) and it's easy to capture.

All of these could be recorded as something like N/A if the information is not able to be determined e.g. if CC isn't set or we're on a platform where we can't detect things.

@sxa

sxa commented Oct 20, 2026

Copy link
Copy Markdown
Member

Going forward we should also look at storing the full configure command with arguments too, although that won't necessarily be practical until we include the SBoM generation in the run-ci target or similar. While many of the options could be inferred from the information we're already capturing in this PR - such as the optional things already discussed above - it would make replicating the build easier if we record that information explicitly so it can be replicated in the future if required. For CycloneDX this can go in a formulation section of the SBoM or included in a separate MBOM document referenced from the SBoM.

My personal preference would probably be inline it so everything relevant is in one place and it's one fewer artifact to worry about but that can be discussed if/when we look at implementing it but 🤷‍♂️ .

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

build Issues and PRs related to Node.js builds or CI infrastructure. needs-ci PRs that need a full CI run. tools Issues and PRs related to the tools directory.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants