Deep dive
Decision table
| Risk | Control | Tooling |
|---|---|---|
| Known-vulnerable dependency | Scan lockfile in CI and on schedule | cargo audit/cargo deny, npm audit, pip-audit, govulncheck, OSV-Scanner |
| Malicious new version | Cooldown before upgrading; review diffs of updates | Renovate minimumReleaseAge, Dependabot cooldown, cargo vet |
| Install-time code execution | Disable lifecycle scripts by default | npm ci --ignore-scripts, pnpm (scripts off for deps by default in v10) |
| Typosquatting / dependency confusion | Scoped/private registry with namespace ownership; explicit registry per scope | .npmrc scopes, cargo registries, pip --index-url only |
| Compromised CI action | Pin by SHA, least permissions:, no secrets to fork PRs | Renovate pins and updates SHAs |
| Stolen publish tokens | Trusted publishing (OIDC) instead of long-lived tokens; 2FA for maintainers | PyPI/npm/crates.io trusted publishing |
| Tampered artifact | Sign + provenance, verify at deploy | cosign, SLSA GitHub generator, npm --provenance |
| Unknown inventory | SBOM per release | Syft, cargo cyclonedx, CycloneDX/SPDX |
| Base image drift | Pin image by digest, rebuild regularly, minimal base | distroless, Chainguard, FROM image@sha256:... |
Code
deny.toml for cargo deny check in CI:
[advisories]
version = 2
yanked = "deny"
[licenses]
version = 2
allow = ["MIT", "Apache-2.0", "BSD-2-Clause", "BSD-3-Clause", "ISC", "Unicode-3.0", "Zlib"]
[bans]
multiple-versions = "warn"
wildcards = "deny"
[sources]
unknown-registry = "deny"
unknown-git = "deny"
Hardened GitHub Actions workflow:
name: release
on:
push:
tags: ["v*"]
permissions: {} # default: nothing
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
id-token: write # keyless signing via OIDC
steps:
- uses: actions/checkout@<full-40-char-sha> # v4.x, pinned
with: { persist-credentials: false }
- run: cargo build --release --locked
- run: cargo deny check
- uses: sigstore/cosign-installer@<full-40-char-sha>
- run: |
docker build -t ghcr.io/org/app:${GITHUB_REF_NAME} .
docker push ghcr.io/org/app:${GITHUB_REF_NAME}
DIGEST=$(docker inspect --format='{{index .RepoDigests 0}}' ghcr.io/org/app:${GITHUB_REF_NAME})
cosign sign --yes "$DIGEST" # sign the digest, never a mutable tag
Verify at deploy (admission controller or deploy script):
cosign verify ghcr.io/org/app@sha256:<digest> \
--certificate-identity "https://github.com/org/app/.github/workflows/release.yml@refs/tags/v1.4.0" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com"
npm defaults (.npmrc in the repo):
ignore-scripts=true
save-exact=true
Efficiency
Cost: CI minutes, network fetches and delayed upgrades, not runtime.
Levers:
- Cache the advisory data —
cargo deny checkandcargo auditread the lockfile plus a cached advisory database, so they take seconds (order of) when the database is cached between CI runs — every CI. - Scan on change and on a schedule — when the lockfile changes plus one daily run, instead of every push — busy repositories.
- Keyless signing — cosign with OIDC adds a few network calls per release (order of seconds) and removes key storage and rotation; sign the digest once per release, not per build — released artifacts.
- Fewer dependencies — shrink scan time, review load and attack surface — always.
- Minimal base images — distroless or Chainguard cut pull size and CVE noise; static images are order of MiB, full distributions order of tens to hundreds of MiB — containers.
Naive → standard → state of the art:
- Naive: unpinned dependencies, scan on every push · unbounded CI minutes, no protection
- Standard: committed lockfile,
--lockedbuilds, SHA-pinned actions,cargo deny· seconds per run - State of the art: add SBOMs, provenance, reproducible builds and keyless signing for high-value artifacts · order of seconds per release
Process
- Fewer dependencies: each one is a maintainer you trust with production. Prefer the standard library and well-staffed packages.
- Updates via bot PRs, batched weekly, with cooldown; security fixes fast-tracked.
- CI on PRs from forks runs without secrets; deploy jobs only on protected branches/tags with required reviews.
- Releases: built in CI only (never laptops), signed, SBOM attached, provenance published.
- Deploy verifies signature identity and digest; runtime images are immutable.
When a dependency is compromised
- Search lockfiles and SBOMs across repos for the bad versions; check build logs for when they were installed.
- Treat any CI job or laptop that ran the bad version as compromised: rotate every secret it could read.
- Pin to a known-good version, rebuild from clean runners, and redeploy.
Pitfalls
- Tags are mutable:
uses: some/action@v3orFROM node:22can change under you. curl | shinstallers in CI and Dockerfiles.- Lockfile present but CI runs
npm install/cargo update, silently ignoring it. pull_request_targetworkflows checking out and running PR code with secrets.- Scanners alerting on hundreds of unreachable CVEs until everyone ignores them; triage by reachability (
govulncheckdoes this) and fix the rest. - Signing but never verifying.
- Vendored code that never gets updated.
- Developer machines with publish tokens and broad cloud credentials: the worm's favorite target.
In production
- Government: CISA publishes SBOM guidance and minimum elements for software producers and buyers.
- Package registries: npm publishes packages with provenance, signed by Sigstore and logged in a public transparency ledger.
- Cloud infrastructure: the Kubernetes release process signs tarballs, SPDX files and binaries with cosign keyless signing; Binary Authorization admits only images with attestations.
Implementations
| name | lang | repo | install | verdict | licence | health | as_of | notes |
|---|---|---|---|---|---|---|---|---|
| cargo-deny | Rust | EmbarkStudios/cargo-deny | cargo install --locked cargo-deny / brew install cargo-deny | — | — | — | — | advisories, licenses, bans, sources |
| cargo-audit | Rust | rustsec/rustsec | cargo install cargo-audit --locked / brew install cargo-audit | — | — | — | — | scans Cargo.lock against RustSec |
| cargo-vet | Rust | mozilla/cargo-vet | cargo install --locked cargo-vet | — | — | — | — | records human audits of dependencies |
| cosign | Go | sigstore/cosign | brew install cosign | — | — | — | — | keyless signing and verification of images and blobs |
| Syft | Go | anchore/syft | brew install syft | — | — | — | — | SBOM generation (CycloneDX, SPDX) |
Sources
- SLSA, Supply-chain Levels for Software Artifacts specification v1.0, OpenSSF, 2023. spec
- Newman, Meyers, Torres-Arias, Sigstore: Software Signing for Everybody, ACM CCS 2022. paper · code
- Ladisa et al., SoK: Taxonomy of Attacks on Open-Source Software Supply Chains, IEEE S&P 2023. paper · arXiv
- NVD, CVE-2024-3094 (xz-utils backdoor). record
- Birsan, Dependency Confusion: How I Hacked Into Apple, Microsoft and Dozens of Other Companies, 2021.
- CISA, Software Bill of Materials (SBOM). site
- npm Docs, Generating provenance statements. docs
- Kubernetes, Verify Signed Kubernetes Artifacts. docs
- Google Cloud, Binary Authorization overview. docs