Dependency and Supply Chain
The SolarWinds breach in 2020 and the XZ Utils backdoor in 2024 made supply chain attacks front-page news. But most supply chain risks engineering teams face are not sophisticated nation-state operations — they are mundane: a popular npm package abandoned and then re-registered by a malicious actor, a transitive dependency pulling in a version with a known CVE, a Docker base image built from a vulnerable OS layer that was never updated.
The dependency graph of a modern application is enormous and mostly invisible to the team that ships it. This post is about making it visible and controllable.
Your Actual Attack Surface
When you npm install express, you are not installing one package. You are installing a tree of packages, each of which has its own maintainers, its own release process, and its own potential for compromise.
You directly control two packages. You transitively depend on six more. Each transitive package can introduce vulnerabilities, and you may not notice until a scanner surfaces a CVE weeks later.
Lockfiles Are Not Optional
Lockfiles (package-lock.json, yarn.lock, poetry.lock, Cargo.lock) pin the exact resolved version of every direct and transitive dependency. Without them, npm install on CI may resolve a different (potentially vulnerable or compromised) version than the one your developer tested.
# npm — always commit the lockfile, always use it in CI
npm ci # installs from lockfile exactly; fails if lockfile is out of date
# Python — Poetry lockfile
poetry install --no-root # uses poetry.lock exactlyDo not delete or ignore the lockfile in .gitignore. It is part of your security posture.
Automated CVE Scanning
Integrate scanning in CI so CVEs in dependencies block merges before they reach production.
# GitHub Actions — using Trivy for dependency and container scanning
name: Security Scan
on: [push, pull_request]
jobs:
trivy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Scan filesystem (dependencies)
uses: aquasecurity/trivy-action@master
with:
scan-type: fs
scan-ref: .
severity: HIGH,CRITICAL
exit-code: 1 # fail the build on HIGH/CRITICAL
ignore-unfixed: true # skip CVEs with no fix available yet
- name: Scan container image
uses: aquasecurity/trivy-action@master
with:
scan-type: image
image-ref: myapp:${{ github.sha }}
severity: HIGH,CRITICAL
exit-code: 1Scanning at PR time catches new vulnerabilities introduced by a dependency update. Also run scans on a schedule (nightly) to catch CVEs disclosed after the last PR.
Generating and Consuming SBOMs
A Software Bill of Materials (SBOM) is a machine-readable inventory of every component in your software. It lets consumers of your software (or your own security team) quickly answer "are we affected by CVE-XXXX-YYYY?"
SBOM formats: SPDX (Linux Foundation) and CycloneDX (OWASP) are the two standards. Pick either; most tools support both.
# Generate an SBOM for a Node project with Syft
syft packages . -o cyclonedx-json > sbom.cyclonedx.json
# Generate an SBOM for a Python project
syft packages . -o spdx-json > sbom.spdx.json
# Generate an SBOM for a container image
syft packages myapp:latest -o cyclonedx-json > sbom-container.cyclonedx.json
# Scan an SBOM for vulnerabilities with Grype
grype sbom:./sbom.cyclonedx.json --fail-on highPublish your SBOM as a build artifact. Attach it to your container image with cosign attest. When a new CVE is disclosed, you can query your published SBOMs to immediately know which services are affected.
Artifact Signing with Sigstore/cosign
Signing build artifacts creates a verifiable chain from source code to deployed artifact. Anyone who pulls your container image can verify it was built by your CI pipeline and not tampered with in transit.
# In CI — sign the container image after push (keyless, using OIDC)
cosign sign --yes myregistry.io/myapp:${GITHUB_SHA}
# Sign the SBOM attestation
cosign attest --yes --predicate sbom.cyclonedx.json \
--type cyclonedx \
myregistry.io/myapp:${GITHUB_SHA}
# In deployment — verify before pulling
cosign verify \
--certificate-identity "https://github.com/myorg/myapp/.github/workflows/build.yml@refs/heads/main" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
myregistry.io/myapp:${GITHUB_SHA}Sigstore keyless signing uses the OIDC token from GitHub Actions (or other CI) as the signing identity — no long-lived signing keys to manage or rotate.
Typosquatting and Dependency Confusion
Two supply chain attack patterns that scanners will not catch because the malicious package has no known CVE:
Typosquatting: reqeusts instead of requests, colourama instead of colorama. Always verify the package name before adding a dependency.
Dependency confusion: Your internal package registry has a package named myorg-utils. If your package manager is configured to check the public registry first, an attacker can publish myorg-utils to npm/PyPI with a higher version number — your CI installs the attacker's version.
# npm — scoped packages prevent confusion for internal packages
# Use @myorg/ prefix; npm.js does not serve scoped packages to non-org registries
npm install @myorg/utils
# .npmrc — configure private registry for specific scope
@myorg:registry=https://nexus.internal.example.com/repository/npm/For Python, configure pip to use your internal index exclusively for internal packages:
# pip.conf
[global]
index-url = https://nexus.internal.example.com/repository/pypi/simple/
extra-index-url = https://pypi.org/simple/
# Or use --no-index for fully air-gapped environmentsPinning Base Images
A Docker image built FROM node:20 may pull a different OS layer next week than it did today. Pin to a specific digest:
# BEFORE — version tag can change
FROM node:20-alpine
# AFTER — pinned to a specific immutable image digest
FROM node:20-alpine@sha256:a1b2c3d4e5f6...
# Or use a specific image version that maps to a digest
FROM node:20.11.1-alpine3.19Automate base image updates with Dependabot or Renovate so your pinned version is updated regularly with security patches while still being deterministic:
# .github/dependabot.yml
version: 2
updates:
- package-ecosystem: docker
directory: "/"
schedule:
interval: weekly
- package-ecosystem: npm
directory: "/"
schedule:
interval: weekly
open-pull-requests-limit: 10Key Takeaways
- Your transitive dependency tree is your real attack surface — direct dependencies are a small fraction of the packages you run in production.
- Lockfiles are a security control: commit them, use
npm cior equivalent in CI, and treat lockfile changes as code changes requiring review. - Automated CVE scanning in CI on both filesystem dependencies and container images is the minimum baseline; add nightly scheduled scans to catch disclosures after merge.
- SBOMs let you answer "are we affected by this CVE?" across your entire fleet in minutes rather than days — generate and publish them as standard build artifacts.
- Sigstore/cosign keyless signing creates a verifiable chain from source to deployed artifact without managing long-lived signing keys.
- Typosquatting and dependency confusion attacks bypass CVE scanners — use scoped packages, configure registry routing explicitly, and review new dependency additions carefully.