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
licensefield. - 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.
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.
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 devexcludes 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-onlygenerates 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.
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 covers the controls an SBOM supports.
- Adding SLSA Provenance to Package Releases records how artefacts were built.
- Checking Dependency Licenses in CI enforces licence policy on the same data.
- Lockfile Management Strategies explains the lockfile the SBOM is built from.