Supply Chain Security
Purpose
Reduce the risk that arrives through your dependencies and your build pipeline — which is now a more common attack path than a vulnerability in your own code.
When to Use
- Adding a dependency.
- Auditing an existing dependency tree.
- Hardening a CI/CD pipeline against compromise.
- Responding to a compromised or malicious package.
Capabilities
- Dependency vetting and risk assessment.
- Lockfile discipline and reproducible builds.
- SBOM generation and vulnerability scanning.
- Artifact signing and provenance attestation.
- CI pipeline hardening.
Inputs
- The dependency tree, direct and transitive.
- The build pipeline and its permissions.
- The advisory feeds relevant to your ecosystem.
Outputs
- A vetted dependency tree with a committed lockfile.
- An SBOM per build, and a scan that fails on critical findings.
- A pipeline where a compromised dependency cannot exfiltrate credentials.
Workflow
- Vet before adding — Who maintains it? When was it last released? How many transitive dependencies does it drag in? A left-pad-sized utility with forty dependencies is a liability, not a convenience.
- Commit the lockfile and install from it —
npm ci, pip install -r requirements.lock, cargo --locked. An install that resolves versions at build time is an install that can pull a compromised patch release.
- Pin CI actions by SHA — A tag is mutable. A compromised action with
pull_request write access can exfiltrate every secret in the pipeline.
- Restrict pipeline permissions — The build job does not need write access to the repository or production credentials. Scope tokens per job.
- Generate an SBOM and scan it — On every build. Fail on critical vulnerabilities in what you actually ship, not in your dev dependencies.
- Sign and attest — Sigstore/cosign for the artifact, with a provenance attestation linking it to the commit and the build.
Best Practices
- The most dangerous dependency is the one you did not choose: a transitive dependency four levels deep, maintained by nobody, that runs an install script.
- Install scripts (
postinstall in npm, setup.py in pip) execute arbitrary code at install time, on developer machines and in CI. Disable them where you can (npm ci --ignore-scripts) and audit them where you cannot.
- Every dependency is code you ship and are responsible for. A 200-line utility is usually cheaper to write than to depend on.
- Typosquatting is real and effective. Check the package name character by character before adding it, especially in a hurry.
- A CI job that can both read a secret and reach the internet can exfiltrate that secret. Minimize the jobs that hold production credentials.
- Automated dependency-update PRs are good, but merging them without review is how a compromised patch release reaches production automatically.
Examples
Pipeline hardening — pinned, scoped, and scanned:
permissions:
contents: read # the default is often write-all. It should not be.
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write # for keyless signing only
attestations: write
steps:
# Pinned by SHA. A tag can be moved by an attacker who compromises the action.
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- run: npm ci --ignore-scripts # lockfile, no arbitrary install-time code
- name: Generate SBOM
run: npx @cyclonedx/cyclonedx-npm --output-file sbom.json
- name: Fail on critical vulnerabilities in shipped dependencies
run: npx osv-scanner --lockfile=package-lock.json --fail-on-severity=critical
- name: Sign the artifact and attest its provenance
uses: actions/attest-build-provenance@v1
with:
subject-path: dist/app.tar.gz
Vetting a dependency before it enters the tree:
npm view left-pad-ish --json | jq '{
maintainers: .maintainers,
last_publish: .time.modified,
dependencies: (.dependencies // {} | keys | length)
}'
# Transitive weight: what does one `npm install` actually bring in?
npm install --dry-run left-pad-ish 2>&1 | grep "added"
# added 47 packages <- for a function you could write in six lines
Responding to a compromised package:
1. Determine exposure: is the compromised version in any lockfile, in any
environment, including CI? `npm ls <pkg>` across every repository.
2. If it ran in CI: every secret that job could reach is compromised. Rotate
all of them. This is the step people skip and it is the one that matters.
3. Pin to a known-good version, rebuild, redeploy.
4. Check for persistence: a malicious postinstall may have written to
~/.npmrc, added a git hook, or modified a lockfile.
Notes
- The critical, frequently-missed step in a package compromise is rotating the CI secrets. If the malicious code executed in a job that had access to a deployment key, that key is compromised — and the package is the least of the problem.
--ignore-scripts breaks packages that legitimately need to compile native code. The correct response is to allow-list those specific packages, not to re-enable scripts globally.
- An SBOM is only useful if something consumes it. Generating one to satisfy a compliance checkbox, and never scanning it, buys nothing.
1---2name: supply-chain3description: Use when assessing dependency and build-pipeline risk. Covers dependency vetting, lockfiles, SBOMs, provenance and signing, pinning CI actions, and responding to a compromised package.4---56# Supply Chain Security78## Purpose910Reduce the risk that arrives through your dependencies and your build pipeline — which is now a more common attack path than a vulnerability in your own code.1112## When to Use1314- Adding a dependency.15- Auditing an existing dependency tree.16- Hardening a CI/CD pipeline against compromise.17- Responding to a compromised or malicious package.1819## Capabilities2021- Dependency vetting and risk assessment.22- Lockfile discipline and reproducible builds.23- SBOM generation and vulnerability scanning.24- Artifact signing and provenance attestation.25- CI pipeline hardening.2627## Inputs2829- The dependency tree, direct and transitive.30- The build pipeline and its permissions.31- The advisory feeds relevant to your ecosystem.3233## Outputs3435- A vetted dependency tree with a committed lockfile.36- An SBOM per build, and a scan that fails on critical findings.37- A pipeline where a compromised dependency cannot exfiltrate credentials.3839## Workflow40411. **Vet before adding** — Who maintains it? When was it last released? How many transitive dependencies does it drag in? A left-pad-sized utility with forty dependencies is a liability, not a convenience.422. **Commit the lockfile and install from it** — `npm ci`, `pip install -r requirements.lock`, `cargo --locked`. An install that resolves versions at build time is an install that can pull a compromised patch release.433. **Pin CI actions by SHA** — A tag is mutable. A compromised action with `pull_request` write access can exfiltrate every secret in the pipeline.444. **Restrict pipeline permissions** — The build job does not need write access to the repository or production credentials. Scope tokens per job.455. **Generate an SBOM and scan it** — On every build. Fail on critical vulnerabilities in what you actually ship, not in your dev dependencies.466. **Sign and attest** — Sigstore/cosign for the artifact, with a provenance attestation linking it to the commit and the build.4748## Best Practices4950- The most dangerous dependency is the one you did not choose: a transitive dependency four levels deep, maintained by nobody, that runs an install script.51- Install scripts (`postinstall` in npm, `setup.py` in pip) execute arbitrary code at install time, on developer machines and in CI. Disable them where you can (`npm ci --ignore-scripts`) and audit them where you cannot.52- Every dependency is code you ship and are responsible for. A 200-line utility is usually cheaper to write than to depend on.53- Typosquatting is real and effective. Check the package name character by character before adding it, especially in a hurry.54- A CI job that can both read a secret and reach the internet can exfiltrate that secret. Minimize the jobs that hold production credentials.55- Automated dependency-update PRs are good, but merging them without review is how a compromised patch release reaches production automatically.5657## Examples5859**Pipeline hardening — pinned, scoped, and scanned:**6061```yaml62permissions:63 contents: read # the default is often write-all. It should not be.6465jobs:66 build:67 runs-on: ubuntu-latest68 permissions:69 contents: read70 id-token: write # for keyless signing only71 attestations: write72 steps:73 # Pinned by SHA. A tag can be moved by an attacker who compromises the action.74 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.27576 - run: npm ci --ignore-scripts # lockfile, no arbitrary install-time code7778 - name: Generate SBOM79 run: npx @cyclonedx/cyclonedx-npm --output-file sbom.json8081 - name: Fail on critical vulnerabilities in shipped dependencies82 run: npx osv-scanner --lockfile=package-lock.json --fail-on-severity=critical8384 - name: Sign the artifact and attest its provenance85 uses: actions/attest-build-provenance@v186 with:87 subject-path: dist/app.tar.gz88```8990**Vetting a dependency before it enters the tree:**9192```bash93npm view left-pad-ish --json | jq '{94 maintainers: .maintainers,95 last_publish: .time.modified,96 dependencies: (.dependencies // {} | keys | length)97}'9899# Transitive weight: what does one `npm install` actually bring in?100npm install --dry-run left-pad-ish 2>&1 | grep "added"101# added 47 packages <- for a function you could write in six lines102```103104**Responding to a compromised package:**105106```text1071. Determine exposure: is the compromised version in any lockfile, in any108 environment, including CI? `npm ls <pkg>` across every repository.1091102. If it ran in CI: every secret that job could reach is compromised. Rotate111 all of them. This is the step people skip and it is the one that matters.1121133. Pin to a known-good version, rebuild, redeploy.1141154. Check for persistence: a malicious postinstall may have written to116 ~/.npmrc, added a git hook, or modified a lockfile.117```118119## Notes120121- The critical, frequently-missed step in a package compromise is rotating the CI secrets. If the malicious code executed in a job that had access to a deployment key, that key is compromised — and the package is the least of the problem.122- `--ignore-scripts` breaks packages that legitimately need to compile native code. The correct response is to allow-list those specific packages, not to re-enable scripts globally.123- An SBOM is only useful if something consumes it. Generating one to satisfy a compliance checkbox, and never scanning it, buys nothing.