# 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

Source: https://socket.dev/glossary/supply-chain-security

Last updated: 2026-10-26

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](https://socket.dev/glossary/supply-chain-levels-for-software-artifacts) 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](https://socket.dev/blog/tanstack-npm-packages-compromised-mini-shai-hulud-supply-chain-attack) 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](https://socket.dev/glossary/software-supply-chain) is the system: dependencies, registries, build systems, CI/CD, publishing accounts, and developer tools. A [supply chain attack](https://socket.dev/glossary/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](https://socket.dev/blog/owasp-2025-top-10-adds-software-supply-chain-failures) 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.

> 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.
>
> The chain of custody for esbuild 0.28.2: five integrity checks pass, and none of them reads the install script that runs at the end

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:

| Hop | Check | What it proves for esbuild 0.28.2 |
| --- | --- | --- |
| Source | Provenance, source | Built from github.com/evanw/esbuild at commit 609683d8 on main |
| Build | Provenance, builder | Built by .github/workflows/publish.yml on a GitHub-hosted runner |
| Publish | Trusted publishing | Uploaded by GitHub Actions through OIDC, with no long-lived npm token |
| Registry | Registry signature | npm signed the tarball with key SHA256:DhQ8wR5A |
| Resolve | Lockfile integrity | package-lock.json pins the sha512 hash, so later installs get the same bytes |
| Install | Approval gate only | Nothing. 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](https://socket.dev/glossary/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](https://socket.dev/blog/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](https://socket.dev/blog/oss-maintainers-feeling-the-weight-of-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.

| Framework | Question it answers | What it explicitly leaves out | Status, October 2026 |
| --- | --- | --- | --- |
| SLSA Build track | Was this artifact built by the expected, tamper-resistant pipeline? | A malicious producer, dependency threats, and typosquatting, per its own threat model | v1.2, released November 24, 2025 |
| SLSA Source track | Did source change only through the expected review process? | Whether reviewed and approved code is malicious | New 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 checks | v1.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 manifest | Optional in US federal procurement since OMB M-26-05, January 23, 2026 |
| OpenSSF Scorecard | Does this upstream project follow good hygiene? | What a specific release contains or does | 18 heuristic checks plus one experimental, each scored 0 to 10 |
| EU Cyber Resilience Act | What must a manufacturer do and report? | Technical method: it sets duties, not controls | Reporting 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](https://socket.dev/blog/federal-government-rescinds-software-supply-chain-mandates-makes-sboms-optional) 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](https://socket.dev/glossary/nist-ssdf) as the checklist for your own development program. Generate a [software bill of materials](https://socket.dev/glossary/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](https://socket.dev/blog/axios-npm-package-compromised), 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](https://socket.dev/blog/pnpm-10-0-0-blocks-lifecycle-scripts-by-default) corrected it on January 7, 2025, and [npm v12](https://socket.dev/blog/npm-12) 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
$ 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
$ 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](https://socket.dev/blog/popular-rust-crates-compromised), 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](https://socket.dev/glossary/postinstall-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](https://docs.socket.dev/docs/socket-firewall-overview) 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](https://socket.dev/glossary/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](https://socket.dev/blog/hidden-blast-radius-of-the-axios-compromise) 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](https://socket.dev/blog/npm-introduces-minimumreleaseage-and-bulk-oidc-configuration) 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
$ 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](https://socket.dev/supply-chain-attacks/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](https://socket.dev/glossary/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](https://socket.dev/blog/npm-to-implement-staged-publishing) that a human approves with 2FA. npm is [phasing out 2FA-bypass granular access tokens](https://socket.dev/blog/npm-12). 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](https://socket.dev/glossary/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](https://socket.dev/blog/nist-nvd-ai-automation) 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.
