Understanding the npm Supply Chain Risk Landscape
The npm ecosystem powers the majority of JavaScript and TypeScript web applications, backend services, and even desktop tools. Every package.json pulls in direct dependencies that in turn pull hundreds of transitive packages. This creates a large attack surface where a single compromised package can affect thousands of downstream applications.
Real incidents illustrate the problem clearly. In 2018 the event-stream package was injected with cryptomining code after its maintainer handed control to a malicious actor. In 2022 the maintainer of colors and faker published versions that broke production builds and contained obfuscated malicious code. These events show that both intentional sabotage and accidental breakage travel through the dependency graph.
A Software Bill of Materials addresses this by producing a complete, machine-readable inventory of every component, its exact version, license, and supplier information. When generated at build time and attached to releases, an SBOM becomes verifiable evidence of what shipped.
CycloneDX vs SPDX: Choosing a Format for Security Workflows
Two main standards exist: SPDX (Software Package Data Exchange) maintained by the Linux Foundation and CycloneDX maintained by OWASP. For security-focused teams CycloneDX offers practical advantages.
CycloneDX includes explicit support for vulnerability information, component pedigree, and cryptographic hashes of artifacts. Its JSON schema is simpler to parse than SPDX Tag-Value or RDF formats. Most modern security tools including Dependency-Track, Grype, and Syft have first-class CycloneDX support.
SPDX remains valuable for legal and compliance teams because of its mature license expression syntax. Many organizations generate both formats. For the patterns below we focus on CycloneDX because it aligns directly with vulnerability scanning and runtime monitoring use cases common in web development teams.
Generating CycloneDX SBOMs from npm Projects
The official tool is @cyclonedx/cyclonedx-npm. It reads your package-lock.json (or generates one if missing) and produces a CycloneDX document containing every dependency.
Install it locally or run it via npx to avoid polluting developer machines:
npx @cyclonedx/cyclonedx-npm --output-format JSON --output-file bom.json --include-dev false
The --include-dev false flag excludes development dependencies, producing a smaller, production-relevant SBOM. For containerized deployments this matches the packages that actually end up in the final image.
Running the command on a medium-sized Next.js application typically yields a JSON file between 400 KB and 1.2 MB depending on transitive dependency count. The resulting bom.json follows the CycloneDX 1.6 schema and contains a components array with entries like this:
{
"bomFormat": "CycloneDX",
"specVersion": "1.6",
"version": 1,
"components": [
{
"type": "library",
"name": "next",
"version": "14.2.5",
"purl": "pkg:npm/next@14.2.5",
"licenses": [{"license": {"id": "MIT"}}],
"hashes": [{"alg": "SHA-512", "content": "..."}]
}
]
}
The Package URL (purl) field is especially useful because downstream tools parse it to map components to vulnerability databases.
Customizing SBOM Generation
The CLI accepts several practical flags. Use --output-format XML when integrating with older enterprise tools that only accept XML. The --mc-type flag lets you declare the main component type (application or library).
For monorepos, run the generator in each package directory and then use the cyclonedx-merge utility to combine them into a single document. This avoids duplicate component entries and gives a unified view of the entire product.
Exclude certain packages known to trigger false positives by creating a .cyclonedxignore file at the project root. Each line accepts a glob pattern or exact package name.
Scanning SBOMs with Grype for Known Vulnerabilities
Grype from Anchore consumes the SBOM directly and matches components against multiple vulnerability databases including NVD, GitHub Advisory Database, and language-specific sources.
Install the latest Grype binary from the GitHub releases page or via Homebrew:
brew install grype
Scan the SBOM with:
grype sbom:bom.json --output table
Typical output shows severity, CVE or GHSA identifier, installed version, and fixed version when available. A sample line might read:
npm: semver 7.5.2 (fixed in 7.5.4) - GHSA-c2qf-rxjj-qqgw - Medium
Grype can exit with a non-zero code when vulnerabilities above a chosen severity are found. Use the --fail-on flag:
grype sbom:bom.json --fail-on medium
This integrates cleanly into CI pipelines where a medium or higher vulnerability should block a release.
Configuring Grype to Reduce Noise
Create a .grype.yaml file to ignore classes of false positives:
ignore:
- vulnerability: GHSA-1234-5678-90ab
- namespace: "github:language:javascript"
package: "lodash"
version: "4.17.21"
severity:
- negligible
- low
The configuration lets teams suppress known issues that have been reviewed and accepted while still catching new high-severity problems.
Signing SBOMs with Sigstore for Verifiable Provenance
An unsigned SBOM can be tampered with. Sigstore provides free, keyless signing backed by short-lived certificates from Fulcio and transparency logs from Rekor.
The cosign CLI signs both container images and arbitrary blobs. For SBOMs the recommended pattern is to sign the JSON file and attach the signature as an attestation.
Install cosign:
go install github.com/sigstore/cosign/v2/cmd/cosign@latest
Keyless signing in CI looks like this:
cosign sign-blob --yes bom.json --bundle bom.json.sigstore.json
The resulting .sigstore.json bundle contains the signature, certificate, and transparency log entry. Consumers can verify it with:
cosign verify-blob --bundle bom.json.sigstore.json --certificate-identity-regexp '.*@github.com' bom.json
This guarantees the SBOM was produced by your GitHub Actions workflow and has not been altered after signing.
Full GitHub Actions Workflow Example
The following workflow runs on every push to main and on release creation. It generates the SBOM, scans it, signs it, and uploads both the SBOM and signature as workflow artifacts plus attaches them to the GitHub release.
name: SBOM Generation and Scanning
on:
push:
branches: [ main ]
release:
types: [ created ]
jobs:
sbom:
runs-on: ubuntu-22.04
permissions:
contents: read
id-token: write
attestations: write
steps:
- uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Generate CycloneDX SBOM
run: npx @cyclonedx/cyclonedx-npm --output-format JSON --output-file bom.json --include-dev false
- name: Scan with Grype
uses: anchore/scan-action@v3
with:
sbom: bom.json
severity-cutoff: medium
fail-build: true
- name: Sign SBOM with Sigstore
uses: sigstore/cosign-installer@v3
- run: cosign sign-blob --yes bom.json --bundle bom.sigstore.json
- name: Upload SBOM and signature
uses: actions/upload-artifact@v4
with:
name: sbom
path: |
bom.json
bom.sigstore.json
- name: Attach to release
if: github.event_name == 'release'
uses: softprops/action-gh-release@v2
with:
files: |
bom.json
bom.sigstore.json
This workflow uses official actions where possible, requires minimal secrets, and produces verifiable artifacts that can be consumed by downstream systems or customers.
Integrating SBOMs with Dependency-Track
Dependency-Track is an open-source platform that ingests CycloneDX SBOMs and continuously monitors them against fresh vulnerability data. After uploading an SBOM, the platform creates a project, tracks metrics, and sends notifications when new vulnerabilities appear in already-released artifacts.
Upload can be done via the REST API or the official CLI. In CI you can add a step after successful scanning:
curl -X POST \
-H "X-Api-Key: ${DT_API_KEY}" \
-F "projectName=MyNextApp" \
-F "projectVersion=1.3.2" \
-F "bom=@bom.json" \
https://dependency-track.example.com/api/v1/bom
Teams running multiple microservices upload each service's SBOM to its own project. Cross-service dependency visibility becomes possible by linking projects through the UI.
SBOM Patterns for Next.js and Frontend Bundles
Next.js applications contain both server-side Node dependencies and client-side bundled JavaScript. The server SBOM is generated exactly as shown above. For the frontend bundle, two approaches exist.
First, generate a separate SBOM for the node_modules used during the build, then use a webpack or Vite plugin that records imported packages. The rollup-plugin-cyclonedx community plugin produces a CycloneDX document from the Rollup bundle graph that Next.js uses internally.
Second, treat the entire built artifact as a single component and reference the server SBOM for the runtime dependencies. This simplifies consumption for customers who only care about runtime attack surface.
In practice most teams generate one SBOM per deployed artifact: one for the Next.js serverless functions or Docker image, and a separate lightweight manifest for static assets when delivering to a CDN.
Performance and Operational Trade-offs
Generating an SBOM adds roughly 4-12 seconds to a typical CI job on a project with 800 transitive dependencies. Scanning with Grype adds another 6-15 seconds because it must download the vulnerability database on first run. Subsequent runs on the same runner benefit from caching the ~/.cache/grype/db directory.
Teams often run the full SBOM generation and scan only on the main branch or during release tagging, while keeping a lighter npm audit check on every pull request. This balances security with developer velocity.
False positive rates vary. Grype correctly identifies many CVEs but JavaScript ecosystem versioning can produce noise when a vulnerable package is listed yet the vulnerable code path is not imported. Manual triage remains necessary for medium-severity findings until teams build ignore lists based on actual usage analysis.
Storage cost is negligible. A typical signed SBOM bundle is under 2 MB. Storing every release SBOM in a GitHub release or S3 bucket adds minimal overhead while providing long-term audit capability.
Advanced Pattern: SBOM Attestation with SLSA Level 3
The Supply-chain Levels for Software Artifacts (SLSA) framework recommends attaching signed provenance to builds. By combining the CycloneDX SBOM with an SLSA provenance statement signed by Sigstore, you reach a strong assurance level.
The slsa-github-generator project provides reusable workflows that produce provenance. You can extend it to also attach the CycloneDX document as a subject of the provenance. The resulting attestation.json can be verified by tools such as slsa-verifier.
This combination—CycloneDX inventory plus cryptographically verifiable build provenance—satisfies many enterprise and government procurement requirements without requiring custom infrastructure.
Consuming SBOMs in Downstream Tools and Processes
Beyond Dependency-Track, several tools consume CycloneDX directly. Microsoft’s sbom-tool can enrich an existing SBOM with additional metadata. The OpenSSF Scorecard can reference SBOM data when calculating supply chain risk scores.
For containerized deployments, tools like Syft generate SBOMs from Docker images while Grype scans them. When the base image already ships its own SBOM (as many distroless images now do), you can merge the application SBOM with the OS SBOM to produce one complete document.
Some organizations publish their SBOMs alongside releases on GitHub. Customers can then scan them before deployment or feed them into their own vulnerability management platforms.
Keeping SBOMs Accurate Across Dependency Updates
SBOMs become stale the moment npm update or npm install runs. The solution is to treat SBOM generation as part of the release process rather than a one-time activity.
Automate dependency updates with tools like Dependabot or Renovate, then require that every merged dependency update triggers the SBOM workflow. The generated SBOM should be committed only for major releases; for every release the SBOM lives inside the published artifact or container image.
When using Docker, copy the bom.json into the final image at a standard location such as /var/sbom/application.cdx.json. This makes the SBOM discoverable by image scanners without requiring external lookup.
Measuring the Impact of an SBOM Program
After three months of consistent SBOM generation, teams typically observe faster vulnerability response times. Instead of searching dependency trees manually when a new advisory appears, security engineers query Dependency-Track to see exact affected versions and release tags.
The existence of signed SBOMs also simplifies customer audits. Enterprises increasingly request SBOMs as part of procurement; having the process already automated removes friction during sales cycles.
Developers benefit indirectly because the same pipeline that generates SBOMs can also enforce minimum standards such as rejecting packages with high-severity known vulnerabilities before they reach production.
Common Pitfalls and How to Avoid Them
- Committing large bom.json files to the repository bloats git history. Generate them only during CI and treat them as build artifacts.
- Using
package.jsoninstead ofpackage-lock.jsonproduces inaccurate version information. Always runnpm cibefore generation to ensure exact versions. - Assuming one SBOM covers the entire monorepo. Generate per package or per deployable unit for accurate pedigree tracking.
- Ignoring license compliance. CycloneDX can record license expressions; feed the SBOM into tools like FOSSology when open-source license compliance is required.
Evolving the Workflow as Standards Mature
CycloneDX 1.6 added support for machine learning model components and service dependencies. As teams adopt more AI features in their web applications they can extend the same SBOM pipeline to declare models, datasets, and inference endpoints.
GitHub is adding native SBOM export for released packages. The patterns shown here remain compatible because they produce standard CycloneDX that GitHub can ingest or that you can attach manually.
The combination of automated generation, immediate scanning, cryptographic signing, and centralized tracking forms a practical foundation for supply chain security that scales from solo developers to growing product teams. Each piece adds verifiable trust without requiring massive infrastructure changes.
Start small: add the CycloneDX generation step and Grype scan to one repository this week. Once the workflow stabilizes, expand the signing and Dependency-Track integration. The concrete commands and workflow shown above have been used successfully in production Node.js, Next.js, and TypeScript codebases and require only minor adaptation to your specific project layout.
