SBOM for npm, pnpm and Yarn projects under the CRA
On this page
The Cyber Resilience Act asks every manufacturer to keep a software bill of materials “in a commonly used and machine-readable format covering at the very least the top-level dependencies” of the product (Annex I, Part II, point 1). For a JavaScript or TypeScript product, almost everything you need is already in your lock file. The hard part is deciding which of the hundreds of packages in node_modules actually ship. This guide covers that, and the commands that turn it into a CycloneDX or SPDX file.
For the regulation itself, see SBOM for the Cyber Resilience Act.
The lock file is the source of truth
package.json says what you asked for, with ranges (^4.19.0). Only the lock file says what was installed: the exact version of every package, direct and transitive, and usually its integrity hash.
| Package manager | Lock file | Direct dependencies recorded in |
|---|---|---|
| npm | package-lock.json (or npm-shrinkwrap.json) |
the lock file itself |
| pnpm | pnpm-lock.yaml |
the lock file itself |
| Yarn (classic and Berry) | yarn.lock |
package.json only |
Commit the lock file and install with npm ci, pnpm install --frozen-lockfile or yarn install --immutable in your build. Otherwise the SBOM describes one install and the product contains another.
Generate the SBOM
npm has had a built-in command since version 9.5:
npm sbom --sbom-format cyclonedx --sbom-type application --omit dev > sbom.cdx.json
It reads node_modules by default; add --package-lock-only to work from the lock file alone, for example before installing. --sbom-format spdx writes SPDX 2.3 instead. The CycloneDX output is version 1.5.
pnpm added its own command in version 11, writing CycloneDX 1.7 or SPDX 2.3:
pnpm sbom --sbom-format cyclonedx --sbom-type application --prod > sbom.cdx.json
For Yarn, or for a monorepo mixing several package managers, the CycloneDX project’s generator cdxgen reads all of them:
npx @cyclonedx/cdxgen -t js -o sbom.cdx.json
Whatever the tool, set --sbom-type application (or the equivalent) when the product is an application rather than a library, and name and version the root component after the product you place on the market, not after the repository.
What actually ships
The CRA is about the product as delivered, so the question is not “what is in node_modules” but “what reaches the customer”. The answer depends on the kind of product.
- A Node.js service or CLI you distribute. What ships is the production tree:
dependenciesand their dependencies. Leaving outdevDependencies(--omit dev,--prod) gives the right list, as long as your team keeps test and build tools out ofdependencies. - A front-end bundle (an app built with Vite, webpack, esbuild or similar). The bundler copies code from packages into the files you ship, and many projects list those packages as
devDependencies, because nothing is installed at run time. Here--omit devwould drop React or Vue from the SBOM. Either keep everything that ends up in the bundle underdependencies, or describe the bundle itself with a tool that reads it, and keep the development dependencies marked as such rather than deleted. - An Electron or desktop app. It ships the Electron runtime, with its own Chromium and Node.js. Electron’s version belongs in the SBOM, and its security fixes matter as much as those of your npm packages. Electron is usually a
devDependency, so--omit devdrops it: add it back, or generate the SBOM without omitting development dependencies.
Development dependencies are still worth recording. A compromised build tool can tamper with what you ship, and CycloneDX lets you keep them in the file, marked as excluded from the product.
Pitfalls specific to npm
- Install scripts run code. Any package with a
preinstallorpostinstallscript runs duringnpm install, on developer machines and in CI. The Shai-Hulud worm spread through npm in 2025 exactly that way, stealing tokens from the machines that installed infected packages.npm ci --ignore-scriptsin CI, or pnpm, which does not run dependencies’ build scripts unless you allow them, limits the risk. - Several versions of one package. npm can install
lodashthree times at three versions. All three belong in the SBOM, and each must be checked for vulnerabilities. - Workspaces. In a monorepo, produce one SBOM per product you place on the market, not one for the whole repository.
- Git and file dependencies (
github:org/repo#commit,file:../lib) have no registry version. Give them a version and a source the SBOM can carry, or vulnerability databases will never match them.
From SBOM to vulnerabilities
npm audit checks the lock file against the GitHub advisory database, and every CycloneDX-aware scanner reads the SBOM. Two things matter for the CRA beyond the raw list:
- Exploited first. A vulnerability listed in CISA’s Known Exploited Vulnerabilities catalogue or the EU Vulnerability Database is strong evidence of active exploitation; once you are aware that one is in your product, Article 14 gives you 24 hours for an early warning. See CVSS, EPSS and KEV.
- Reachable or not. Most vulnerable functions in a
node_modulestree are never called by your code. Record your assessment in a VEX statement, with the reason, rather than leaving the finding open.
How Provetry helps
Provetry reads package-lock.json, npm-shrinkwrap.json, pnpm-lock.yaml (lock file versions 6 and 9) and yarn.lock (classic and Berry) from your CI or from the files you upload, tells direct from transitive dependencies, and writes the SBOM of each release in CycloneDX, and in SPDX on paid plans. It watches every component for new and exploited vulnerabilities, every day on paid plans and every week on Free. Its CI step looks for evidence that a vulnerable package is never loaded; on the Pro and Consultant plans, it goes down to the vulnerable function for npm. With npm and pnpm lock files, which record them, packages with an install script always count as loaded. Every such finding is a suggestion with its evidence, which you accept or not.