Most of Your Code Isn't Yours
A modern app is 80–95% third-party code by line count. npm install, pip install, andnuget restore are the highest-leverage points in your supply chain — and they execute arbitrary code on developer machines and CI runners. Supply-chain attacks have moved from theoretical to weekly news (event-stream, ua-parser-js, colors.js, xz utils, polyfill.io).
- You haven't run a fresh
npm auditin months - Dependabot PRs sit open until they conflict, then get closed
- You don't know which CVEs exist in your production image
- Your lockfile is .gitignored “because reproducible builds are slow”
The 2026 baseline is: pinned lockfiles, automated bump PRs, an SBOM in CI, a CVE policy with SLAs, and signed artifacts end-to-end.
The Attack Surface
flowchart LR
DEV[Dev machine] -->|install| REG[Public registry]
CI[CI runner] -->|install| REG
REG -->|tarball + install scripts| BUILD[Build]
BUILD --> ART[Artifact / image]
ART --> PROD[Production]
classDef bad fill:#fee
class REG bad
Each arrow is an attack surface. Compromise the registry account → poisoned package. Compromise the build → backdoored artifact. Compromise the image base → escalation in production. The defenses below apply to each.
Pin and Lock Everything
Lockfiles (package-lock.json, pnpm-lock.yaml, requirements.txt with hashes, Pipfile.lock,poetry.lock, packages.lock.json) record the exact resolved version and content hash of every transitive dependency. Commit them. Use --frozen-lockfile / npm ci in CI so a sneaky semver bump never enters the build.
Vulnerability Scanning
Three layers, three tools:
- Source dependencies: Dependabot, Snyk, Renovate, GitHub Advanced Security, OSV-Scanner.
- Container images: Trivy, Grype, Docker Scout, Snyk Container.
- Running infrastructure: cloud-native (Defender for Cloud, AWS Inspector, GCP SCC).
# Combined CI step — fail on critical with a known fix
- name: OSV-Scanner (source)
uses: google/osv-scanner-action@v1
with: { scan-args: '-r --skip-git ./' }
- name: Trivy (image)
uses: aquasecurity/trivy-action@master
with:
image-ref: ghcr.io/me/app:${{ github.sha }}
severity: CRITICAL,HIGH
ignore-unfixed: true
exit-code: '1'
format: sarif
output: trivy.sarif
- name: Upload to GitHub Security
uses: github/codeql-action/upload-sarif@v3
with: { sarif_file: trivy.sarif }CVE Triage SLAs
Not every CVE is your CVE. A vulnerability in a transitive dep's test fixture is not the same as one in your auth path. UseEPSS (Exploit Prediction Scoring System) and reachability analysis to prioritize:
| Class | SLA |
|---|---|
| Critical CVSS, known exploit, reachable | 72 hours |
| High CVSS, reachable code path | 14 days |
| Medium, no exploit | 30 days |
| Low / unreachable | Best effort, suppress with reason |
SBOMs — Know What's in the Box
A Software Bill of Materials is a machine-readable list of every component in your artifact, in CycloneDX or SPDX format. Generate one per build, attach it to the release, and feed it back into your vulnerability scanner on new CVEs — the next Log4Shell is the day you're glad you have one.
# Generate SBOMs in CI
syft packages dir:. -o cyclonedx-json > sbom.cdx.json
syft packages ghcr.io/me/app:1.2.3 -o spdx-json > sbom.spdx.json
# Query an existing SBOM against the OSV database
osv-scanner --sbom sbom.cdx.json
# Attach as a release artifact, and (in 2026) sign it
cosign attest --type cyclonedx --predicate sbom.cdx.json ghcr.io/me/app:1.2.3Lockfile Integrity
Lockfiles record integrity hashes; package managers verify them on install. But review the PR diffs — a lockfile change with no manifest change is suspicious. Tools like lockfile-lint (npm) and pip-audit add additional checks. For deterministic supply chains, vendor critical dependencies or mirror via Artifactory/Nexus with a known-good cache.
Dependency Confusion
Public registries allow anyone to publish a package by name. If your internal package @acme/utilsisn't reserved on npm, an attacker publishes a public one with a higher version — your default resolver may pull theirs instead of yours. Defenses:
- Reserve internal scopes on public registries (
@acmeon npm, prefixes on PyPI/NuGet). - Use scoped registries (
.npmrcper scope, NuGet config per source) so internal scopes only resolve from your private feed. - Disable upstream search for internal scopes in your private registry config.
Sigstore and Signed Artifacts
The end-state: every artifact is signed, every signature is verifiable, every consumer enforces a policy on what signers it accepts.Sigstore (cosign + Rekor transparency log + Fulcio short-lived certs) makes this practical without managing key material. Container images, npm packages (via provenance), and GitHub releases all support signing now.
# Sign a container image with OIDC identity in CI (no long-lived keys)
cosign sign --yes ghcr.io/me/app:1.2.3
# Verify at deploy time — kyverno / policy controller enforces this in-cluster
cosign verify ghcr.io/me/app:1.2.3 \
--certificate-identity-regexp 'https://github.com/me/app/.*' \
--certificate-oidc-issuer https://token.actions.githubusercontent.comSLSA — A Framework for “Where Did This Come From?”
SLSA (Supply-chain Levels for Software Artifacts) defines maturity levels for build provenance. SLSA-3 means your build is hosted, isolated, and produces signed provenance. GitHub Actions, GitLab CI, and Google Cloud Build all support generating SLSA provenance documents out-of-box.
Pitfalls
“We'll patch when there's an incident”
That's called “running unpatched until the breach”. Automate bump PRs with Renovate/Dependabot, group low-risk updates, merge weekly. The cost of a steady drip is much lower than the cost of a yearly forced migration.
Trusting GitHub Actions by ref
uses: actions/checkout@v4 resolves to whatever the maintainer tags as v4. Pin to a full SHA for actions outside the GitHub org. Renovate can auto-bump pinned SHAs while keeping the pin discipline.
Treating SBOMs as a compliance checkbox
An SBOM you generate once and forget is paperwork. An SBOM you re-scan against new vulnerability feeds nightly is a security control. Pick the second one.

