Back to glossary

Software Supply Chain Security

Software supply chain security verifies that the dependencies, tools, builds, and registries your software relies on are authentic and safe before they run.

Also known as SSCS, open source supply chain security, OSS supply chain security

Software supply chain security (SSCS), also called open source supply chain security, is the practice of verifying everything code passes through before it runs: dependencies, build tools, CI pipelines, and package registries. It asks two questions about each artifact. Is this what its producer built? Is what they built safe to run? Most programs only answer the first.

The first question, integrity, is largely solved with cryptography. A lockfile pins a content hash, npm signs the tarballs it serves, and SLSA provenance records the repository, commit, and workflow that built a package. The second question, intent, is not. None of those checks reads the code. The SLSA specification is explicit about a malicious producer: "This kind of attack cannot be directly mitigated through SLSA controls."

That gap is where attacks now land. In May 2026, the TanStack compromise pushed 84 malicious npm artifacts through the project's own trusted-publisher binding, with valid Sigstore provenance. Our position: authenticity checks are necessary but not sufficient, and CVE scanning misses malware because malware rarely gets a CVE. The control that matters most sits where code is fetched and executed, before install, not after deploy.

What is software supply chain security?

#

Software supply chain security is the set of controls that keep malicious or compromised code from reaching your builds, developer machines, and production through things you consume rather than code you write. The software supply chain is the system: dependencies, registries, build systems, CI/CD, publishing accounts, and developer tools. A supply chain attack is what happens when one link is compromised. Software supply chain security is the discipline of defending every link.

Three boundaries keep the term precise. Application security covers flaws in code your team wrote. Software supply chain security covers code and tooling your team consumes, and the pipeline that turns your source into published artifacts. Cyber supply chain risk management (C-SCRM) is broader still, covering vendors, hardware, and procurement.

The category is now mainstream. The OWASP Top 10 2025 added Software Supply Chain Failures as a new category, ranked third. Half of OWASP's community survey respondents ranked it their top concern.

Integrity and intent are separate questions

#

A package install is a chain of custody. A maintainer commits code. A CI job builds it. A publisher uploads it. A registry stores and serves it. A resolver picks a version, and a package manager writes it to disk. Each hop now has an integrity check.

Here is what the npm registry records for esbuild 0.28.2, the current release of a popular JavaScript bundler, as of October 26, 2026 (trimmed):

JSON
{
  "_npmUser": {
    "name": "GitHub Actions",
    "email": "npm-oidc-no-reply@github.com",
    "trustedPublisher": { "id": "github" }
  },
  "dist": {
    "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==",
    "signatures": [{ "keyid": "SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U", "sig": "MEYCIQCU..." }],
    "attestations": {
      "url": "https://registry.npmjs.org/-/npm/v1/attestations/esbuild@0.28.2",
      "provenance": { "predicateType": "https://slsa.dev/provenance/v1" }
    }
  },
  "scripts": { "postinstall": "node install.js" }
}

Every integrity check passes. The publisher is GitHub Actions through trusted publishing, so no long-lived token was involved. Decoding the provenance attestation gives the repository github.com/evanw/esbuild, the workflow .github/workflows/publish.yml, commit 609683d892977362a0f99026cb74b96263d728a9, and a GitHub-hosted runner. The registry signed the tarball. The project's package-lock.json records the same sha512 integrity hash, so the next npm ci will refuse different bytes.

The last line is the part none of those checks look at. Under npm 11 and earlier, node install.js runs automatically on every install. npm 12 holds it until someone approves it, but nothing reads it for them. That script executes the platform-specific binary to check its version, and if the binary is missing, it shells out to npm install or downloads a tarball from registry.npmjs.org. Those are benign reasons. A hijacked release with the same capabilities would be a dropper, and every check above would still pass.

Chain of custody for esbuild@0.28.2

npm registry and package-lock.json, October 26, 2026. Code flows top to bottom.

Integrity: is this what the producer built? Checked 5 waysIntent: is it safe to run? Checked 0 ways
  1. SourceProvenance: source

    Built from github.com/evanw/esbuild at commit 609683d8… on main

  2. BuildProvenance: builder

    Workflow .github/workflows/publish.yml on a GitHub-hosted runner

  3. PublishTrusted publishing

    Uploaded by GitHub Actions through OIDC, with no long-lived npm token

  4. RegistryRegistry signature

    npm signed the tarball, key SHA256:DhQ8wR5A…

  5. ResolveLockfile integrity

    package-lock.json pins sha512-HKVLS8dv…, so later installs get the same bytes

  6. InstallGate, not a check

    postinstall: node install.js spawns processes and can download from the network. npm 11 and earlier run it automatically. npm 12 waits for a human to approve it. Neither reads it.

The gap: every check above is about where the bytes came from. None of them reads what install.js does. A hijacked release built by the same workflow would pass all five.
Figure 1. Five integrity checks pass for esbuild 0.28.2. The only hop that executes code has, at most, a human approval gate. Values are from the registry record, the decoded provenance attestation, and the project lockfile.

Figure 1 shows the chain of custody for esbuild 0.28.2, top to bottom, with the check at each hop and what it proves:

HopCheckWhat it proves for esbuild 0.28.2
SourceProvenance, sourceBuilt from github.com/evanw/esbuild at commit 609683d8 on main
BuildProvenance, builderBuilt by .github/workflows/publish.yml on a GitHub-hosted runner
PublishTrusted publishingUploaded by GitHub Actions through OIDC, with no long-lived npm token
RegistryRegistry signaturenpm signed the tarball with key SHA256:DhQ8wR5A
ResolveLockfile integritypackage-lock.json pins the sha512 hash, so later installs get the same bytes
InstallApproval gate onlyNothing. The postinstall script spawns processes and can reach the network. npm 11 and earlier run it automatically, and npm 12 waits for a human approval.

Five integrity checks pass. The only hop that executes code has, at most, a human approval gate, and no check reads the script.

Each check answers a narrower question than teams tend to assume:

  • Registry signatures prove the registry served the bytes that were uploaded.
  • Package provenance proves which repository, commit, and workflow produced the bytes.
  • Trusted publishing proves the upload came from the configured CI workflow, not from a long-lived token.
  • A lockfile integrity hash proves you received the same bytes as last time.

None of them proves the bytes are benign. The TanStack payload carried valid provenance because, as our report put it, "an attacker who can execute in GitHub Actions can generate valid Sigstore attestations for malicious packages."

Provenance still pays off when a policy checks it against an expectation. The LiteLLM compromise is the clearest case: the malicious 1.107.0 release workflow recorded a source repository named Berri-AI/litellm, one hyphen away from the real BerriAI/litellm. A rule pinning each package to its expected source repository would have rejected it. Provenance that no policy checks is a record, not a control.

Which frameworks cover software supply chain security?

#

Software supply chain security has four common frameworks: SLSA, NIST's SSDF, SBOM requirements, and OpenSSF Scorecard. The EU Cyber Resilience Act adds legal duties. Each answers a different question, and none of them inspects what a specific release does when it runs.

FrameworkQuestion it answersWhat it explicitly leaves outStatus, October 2026
SLSA Build trackWas this artifact built by the expected, tamper-resistant pipeline?A malicious producer, dependency threats, and typosquatting, per its own threat modelv1.2, released November 24, 2025
SLSA Source trackDid source change only through the expected review process?Whether reviewed and approved code is maliciousNew in v1.2
NIST SSDF (SP 800-218)Does the producer follow secure development practices?How to verify a given artifact: it lists high-level practices, not checksv1.1 final February 3, 2022; v1.2 still a draft since December 17, 2025
SBOM (SPDX or CycloneDX)Which components did we ship?What those components do, and anything outside a manifestOptional in US federal procurement since OMB M-26-05, January 23, 2026
OpenSSF ScorecardDoes this upstream project follow good hygiene?What a specific release contains or does18 heuristic checks plus one experimental, each scored 0 to 10
EU Cyber Resilience ActWhat must a manufacturer do and report?Technical method: it sets duties, not controlsReporting duties since September 11, 2026; most obligations from December 11, 2027

The SLSA threat model is unusually candid about its scope. On dependencies it says: "This version of SLSA does not explicitly address dependency threats, but we expect that a future version will." On typosquatting: "This threat is not currently addressed by SLSA." OpenSSF Scorecard is equally direct about its limits: "The checks themselves are heuristics; there are false positives and false negatives."

US federal policy has moved from mandates to risk-based choices. OMB memo M-26-05 rescinded the federal secure software attestation requirement and made SBOMs a contracting option rather than a rule. The Cyber Resilience Act went the other way. Since September 11, 2026, manufacturers of products with digital elements sold in the EU must send an early warning about an actively exploited vulnerability within 24 hours and a notification within 72 hours.

Our verdict: use SLSA for the artifacts you build, and use its provenance to reject packages built outside their expected source. Use the NIST SSDF as the checklist for your own development program. Generate a software bill of materials for what you ship to customers and regulators. Use Scorecard to choose between candidate dependencies. Then accept that none of these tells you whether the next release of a dependency is malicious. That requires analyzing the artifact, and no framework currently requires anyone to do it.

How do you improve software supply chain security?

#

You improve software supply chain security by placing controls where attacks execute, starting at install time, where most of them run, then working outward to ingest, resolution, CI, and publishing. Vulnerability triage comes last. Every control below has a cost and a blind spot.

Stop running dependency code at install time

#

Install time is where most supply chain attacks execute. In the axios compromise, the malicious releases added one dependency, plain-crypto-js@4.2.1, whose postinstall hook ran a dropper for a remote access trojan. Default-on dependency lifecycle scripts were a design mistake. pnpm 10 corrected it on January 7, 2025, and npm v12 on July 8, 2026, according to the registry's publish times for both releases.

Installing esbuild with npm 12.2.1 shows what the new default does:

Shell Session
$ npm install esbuild
added 2 packages, and audited 3 packages in 3s
found 0 vulnerabilities
npm warn install-scripts 1 package had install scripts blocked because they are not covered by allowScripts:
npm warn install-scripts   esbuild@0.28.2 (postinstall: node install.js)
$ npm install-scripts approve esbuild
Approved esbuild:
  added esbuild@0.28.2

The approval lands in package.json as "allowScripts": { "esbuild@0.28.2": true }. The pin to an exact version matters. When we installed esbuild 0.28.1 into the same project, npm blocked its script again, because nobody had reviewed that version. Any future release with an install script, including a hijacked one, needs a fresh human approval. Passing --no-allow-scripts-pin writes a name-only entry that approves every future version, which throws that protection away.

In CI, turn the warning into a failure:

Shell Session
$ npm install esbuild@0.28.1 --strict-allow-scripts
npm error code ESTRICTALLOWSCRIPTS
npm error --strict-allow-scripts: 1 package(s) have install scripts not covered by allowScripts:
npm error   esbuild@0.28.1 (postinstall: node install.js)

npm 12 also refuses git and remote-URL dependencies by default, through allow-git=none and allow-remote=none. Git dependencies were a way to run code even under --ignore-scripts: a git dependency's .npmrc could override the git executable.

What this does not cover: scripts you approved, code that runs when the package is imported, Python .pth startup hooks like the one in LiteLLM 1.107.0, and Cargo build scripts. In the arrayref, internment, and append-only-vec compromise, Cargo ran the typosquat proc-macro1's build.rs during compilation. The cost is real too: a skipped native build does not fail the install, it fails later at runtime. More detail is in our entry on install scripts.

Inspect each artifact where it enters

#

Behavior analysis reads what a package does, not what its metadata claims. It is the only control on this list that addresses intent. The natural place to run it is the point where code enters your environment, because that point sees transitive dependencies, npx runs, and global installs that never appear in a project file.

Socket Firewall works at that point. Socket's documentation describes it as "an HTTP/HTTPS proxy server that sits between package managers and package registries." For each request it checks the package against Socket's analysis and either forwards it or blocks it. The free version wraps npm, yarn, pnpm, pip, uv, and cargo commands, as in sfw npm install.

Socket Firewall's limits are documented. The free tier blocks only malware confirmed by human review and warns on packages flagged by AI that are not yet confirmed. Cached artifacts generate no network request, so the proxy never sees them. Unknown or unscanned versions are not blocked. Detection speed matters too. Socket's scanner flagged the LiteLLM wheels seven minutes after publication, and a proxy cannot block on an alert that does not exist yet. A package firewall narrows the exposure window. It does not close it.

Control when new code enters

#

A lockfile with npm ci makes existing installs deterministic. It does nothing for the next resolution. Our axios follow-up put it plainly: "Lockfiles make existing installs deterministic. They do not make new installs safe." Code fetched by npx resolves fresh from the registry, so in CI run npx --no --offline to execute only what is already installed.

A dependency cooldown refuses versions younger than a threshold. With npm 12's min-release-age set to 90 days on October 26, 2026, a range of ^0.28.0 resolved to esbuild 0.28.1, published June 11, instead of 0.28.2, published August 8:

Shell Session
$ npm install 'esbuild@^0.28.0' --min-release-age=90 --ignore-scripts
$ npm ls esbuild
`-- esbuild@0.28.1

The cost is that security fixes wait too. npm's documentation says that when the window stops npm audit fix from installing a patched version, npm "keeps the package at its vulnerable version, warns that the fix was blocked, and exits with a non-zero code." The escape hatch is min-release-age-exclude. A cooldown also does nothing against a patient attacker, because malicious code does not become safe with age. We treat a short cooldown as a cheap backstop that buys time for ingest-side detection, not as a control on its own.

Treat CI as production

#

Pin every third-party action to a full commit SHA. In March 2026, the Trivy GitHub Actions compromise force-pushed 75 of 76 version tags of aquasecurity/trivy-action to a payload that stole CI secrets, and more than 10,000 workflow files referenced the action. A tag is a pointer anyone with write access can move. A SHA is not.

Never run untrusted pull request code in a workflow that has secrets or a publishing identity. The TanStack chain combined a pull_request_target workflow, cache poisoning across the fork boundary, and an OIDC token read from runner memory. Scope each job's token permissions to what it needs. These are the core of GitHub Actions security. They do not help when the pinned commit is itself malicious, and they do not inspect the dependencies a job installs.

Remove standing publish credentials

#

Publish through trusted publishing or a staged publish that a human approves with 2FA. npm is phasing out 2FA-bypass granular access tokens. When npm 12 shipped, npm said the first step, expected in early August 2026, would block those tokens from sensitive account, package, and organization actions, and that a later step, expected around January 2027, would remove their ability to publish directly. Trusted publishing removes the long-lived-token path. It does not protect a workflow an attacker can run code in, as TanStack showed.

Triage vulnerabilities by reachability

#

Most CVE alerts are noise. A known vulnerability matters when your code can reach the vulnerable function, which reachability analysis determines, not when its CVSS score is high. The CVE pipeline cannot be the only source of truth either. In April 2026, NIST stopped trying to enrich every CVE in the National Vulnerability Database. As of August 17, 2026, 42,353 CVEs sat in its "Not Scheduled" status, against 2,426 awaiting enrichment. And a CVE scanner, however good, will not report malware that nobody has filed a CVE for.

Nobody attests to what a package does

#

The npm ecosystem now has a full chain of cryptographic custody: trusted publishing, Sigstore signing, SLSA provenance, registry signatures, and lockfile hashes. Every link answers who built an artifact and whether it arrived intact. No standard artifact answers what the release does.

The signal that separates esbuild 0.28.2 from the axios dropper is not the presence of an install script, a child process, or a network call. esbuild has all three. The signal is change: the axios release added a dependency nobody had seen, which ran a script nobody had reviewed. That is a behavioral diff between versions, and today only analyzers compute it, outside every framework. SLSA lists a dependency track as future work. Our view is that it should do for behavior what the Build track did for builds: require consumers to record that each new version was analyzed before it was allowed to run.