Back to publishing & release Automate semantic versioning Manage release channels Harden the supply chain

Generating an SBOM for a JavaScript Package

A software bill of materials (SBOM) is a machine-readable inventory of every component in a piece of software: each package, its exact version, its licence, where it came from and how it relates to the others. Customers, auditors and regulators increasingly ask for one, and during a vulnerability response an SBOM answers "are we affected?" in seconds instead of hours. For JavaScript, the lockfile already contains most of the data; the job is to export it in a standard format — CycloneDX or SPDX — at the right moment and attach it to each release. This guide generates SBOMs with npm's built-in command and dedicated tools, explains what to include, and wires generation into CI.

What an SBOM contains

Both standard formats describe the same essentials:

  • Components — every package in the dependency tree, with name, version and a Package URL (pkg:npm/%40acme/ui@2.4.0), which identifies it unambiguously across ecosystems.
  • Hashes — integrity values, usually taken from the lockfile, so a component can be verified.
  • Licences — from each package's license field.
  • Relationships — which component depends on which, so you can trace how a vulnerable package entered the tree.
  • Metadata — the tool that produced it, the time, and the subject: your application or package at a specific version.

The broader supply-chain context is covered in Supply-Chain Security Hardening.

From lockfile to published SBOM The installed tree and lockfile provide components and hashes; an SBOM tool writes CycloneDX or SPDX; CI validates it and attaches it to the release. lockfile + node_modules exact versions, integrity SBOM tool npm sbom / cyclonedx-npm validate schema + completeness attach to release artefact, registry, attestation
The lockfile is the source of truth; the SBOM is a standard, shareable view of it for one release.

CycloneDX or SPDX?

Both are widely accepted. CycloneDX (an OWASP standard) is common in security tooling and vulnerability management; SPDX (a Linux Foundation standard, also ISO/IEC 5962) is common in licence compliance and some government requirements. Many organisations generate both. If a customer or regulator specifies a format, use that one; otherwise CycloneDX JSON is a practical default for JavaScript projects.

CycloneDX versus SPDX for JavaScript projects Compares the two SBOM standards on origin, typical use, JSON support, vulnerability data and tool support in the npm ecosystem. CycloneDX SPDX Maintained by OWASP Linux Foundation (ISO/IEC 5962) Typical focus security and VEX licensing and compliance npm sbom support yes yes Dedicated npm tooling @cyclonedx/cyclonedx-npm via converters Dependency relationships yes yes
Either format satisfies most requests; pick the one your consumers ask for, or generate both.

Generating with npm

npm 10 and later include an sbom command that reads the installed tree and lockfile:

npm ci
npm sbom --sbom-format cyclonedx --omit dev > sbom.cdx.json
npm sbom --sbom-format spdx --omit dev > sbom.spdx.json
  • --omit dev excludes development dependencies, which do not ship — include them only if your policy requires a build-environment inventory.
  • --sbom-type (library, application, framework) describes the subject.
  • --package-lock-only generates from the lockfile without an installed tree, useful in lightweight CI steps.
  • In npm workspaces, --workspace <name> produces an SBOM for one package.

For pnpm and Yarn projects, check whether your package manager version offers native SBOM generation; otherwise the CycloneDX tool works from an installed node_modules:

npx @cyclonedx/cyclonedx-npm --omit dev --output-format JSON --output-file sbom.cdx.json

Check your pnpm version's documentation for native support, and prefer a tool that reads the lockfile your project actually uses, so the SBOM matches what was installed.

SBOMs for libraries versus applications

The subject matters. For a deployed application, the SBOM should describe exactly what runs in production: every resolved production dependency, generated from the lockfile used for the deployment build. For a published library, the runtime tree is decided by each consumer's install, so an SBOM of your lockfile describes your development and test environment, not what consumers get. Libraries typically publish an SBOM of their direct dependencies and ranges (useful for licence review) and rely on consumers to generate application SBOMs from their own lockfiles.

What the SBOM should describe Applications describe their resolved production tree; libraries describe their declared dependencies; container images describe everything in the image. What are you releasing? the SBOM subject Resolved prod tree from the deploy build's lockfile application Declared dependencies consumers resolve their own npm library Whole image OS packages + node_modules container image
Generate the SBOM for the artefact you actually ship.

For container images, use an image-level tool (for example Syft or Docker's SBOM support), which captures operating system packages as well as node_modules.

CI integration

Generate the SBOM in the same job that builds the release, from the same install, and attach it to the release:

jobs:
  release:
    runs-on: ubuntu-latest
    permissions:
      contents: write
      id-token: write
      attestations: write
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 24 }
      - run: npm ci
      - run: npm run build
      - run: npm sbom --sbom-format cyclonedx --omit dev > sbom.cdx.json
      - name: Validate SBOM is non-trivial
        run: test "$(jq '.components | length' sbom.cdx.json)" -gt 0
      - uses: actions/attest-sbom@v2
        with:
          subject-path: dist/app.tgz
          sbom-path: sbom.cdx.json
      - uses: softprops/action-gh-release@v2
        with:
          files: |
            dist/app.tgz
            sbom.cdx.json

The attestation step signs a statement binding the SBOM to the release artefact, so consumers can verify the SBOM belongs to the exact file they received — complementing npm provenance, covered in Adding SLSA Provenance to Package Releases.

Monorepos: one SBOM per shipped artefact

A monorepo that deploys several applications and publishes several packages should produce one SBOM per shipped artefact, not one for the whole repository. A repository-wide SBOM lists every dependency of every application together, which overstates each application's exposure and makes vulnerability triage noisy: a vulnerable package used only by the admin tool would appear to affect the public website too.

Generate per-application SBOMs from each application's production dependency closure. With npm workspaces, npm sbom --workspace <name> --omit dev scopes the output. With pnpm, deploy the application to a folder first (pnpm --filter @acme/web deploy --prod /tmp/web) and run the SBOM tool against that folder, which contains exactly the application's production tree — the same technique described in Deploying a Single Package with pnpm deploy. Internal workspace packages then appear as components with their workspace versions, which is accurate: they are part of what ships.

Keeping SBOMs accurate

An SBOM is a claim about what shipped, so treat its accuracy like any other release property. Three failure modes are common. Generating from the wrong install — for example, from a developer's node_modules instead of the CI build — produces an SBOM that does not match the artefact. Including development dependencies inflates the component list with test runners and linters that never ship, which triggers irrelevant vulnerability matches. Missing bundled code: if your build bundles dependencies into a single file, the SBOM from node_modules still lists them correctly, but code copied into the repository (vendored snippets, inlined libraries) does not appear at all; list such components manually in the SBOM metadata, or better, consume them as real dependencies.

Validating against the schema catches malformed files; comparing the component count with the previous release catches the other failures. A sudden drop usually means the SBOM was generated against an incomplete install, and a sudden jump often means dev dependencies crept in.

Using the SBOM

An SBOM is only valuable if someone can query it. Feed release SBOMs into a vulnerability management system (Dependency-Track is a common open-source choice) so new advisories are matched against every released version automatically. When an incident hits — a compromised package, a critical vulnerability — you can then answer "which releases include ansi-regex@6.2.1?" across all applications at once, which is the first question in Responding to a Compromised Dependency.

Worked example: an SBOM requirement in a customer contract

A company selling software to a public-sector customer must provide an SBOM with each release. They add npm sbom --sbom-format spdx --omit dev to the release job of each deployed application, attach the files to GitHub releases, and upload CycloneDX versions to an internal Dependency-Track instance. Two months later, a vulnerability disclosure in a transitive parsing library arrives; Dependency-Track shows within minutes that three of eleven applications ship the affected version, and those three get patched releases the same day.

Prevention and guardrails

  • Generate SBOMs from the same install as the release build.
  • Exclude dev dependencies for shipped-software SBOMs unless policy requires them.
  • Validate SBOMs in CI — non-empty, schema-valid, correct subject version.
  • Store and query them; an SBOM nobody reads provides no protection.

Frequently Asked Questions

Does the SBOM include licence information? Yes, from each package's license field. Licence policy enforcement is a separate step, covered in Checking Dependency Licenses in CI.

Should the SBOM be committed to the repository? Usually not. It describes a release, so attach it to the release artefact or store it in your SBOM management system. Committing it creates noisy diffs on every dependency change.

Is an SBOM the same as provenance? No. An SBOM lists what is inside an artefact; provenance describes how and where the artefact was built. Both are useful and they complement each other.

How often should SBOMs be regenerated? With every release, because every release may change dependencies. For long-lived deployed versions, keep the SBOM produced at release time — it documents what shipped — and let your vulnerability system re-evaluate it against new advisories.

What is VEX, and do I need it? VEX (Vulnerability Exploitability eXchange) documents whether a known vulnerability in a listed component actually affects your product. It pairs with an SBOM to reduce false alarms for customers; CycloneDX supports it natively. Start with SBOMs, and add VEX statements when customers ask about specific advisories.

Related

Supply-Chain Security Hardening