Supply Chain Security: Managing the 95% You Didn’t Write
Modern applications are assembled more than written. A typical Node.js project might have 1,200 dependencies when you only directly installed 15. A Python Flask app pulls in 40+ packages. A Java Spring Boot application can have 200+ JARs.
Each dependency is a trust relationship. Each one can have vulnerabilities. Each one could be compromised.
Supply chain security is about managing that trust systematically rather than hoping for the best.
Understanding the Supply Chain Attack Surface
Your application’s supply chain is everything between source code and running production:
Direct vs Transitive Dependencies
Direct dependencies are packages you explicitly install:
{
"dependencies": {
"express": "^4.18.2",
"react": "^18.2.0",
"axios": "^1.6.0"
}
}
Transitive dependencies are what your dependencies depend on. Install express and you get 30+ additional packages. You didn’t choose them, but they run in your application with the same permissions as code you wrote.
Run this to see the dependency tree:
# npm
npm ls
# Python
pip show <package> | grep Requires
# Go
go mod graph
# Rust
cargo tree
Most vulnerabilities are in transitive dependencies you didn’t know existed.
The Full Supply Chain
- Dependencies: Libraries and frameworks you import
- Build tools: Compilers, bundlers, task runners (Webpack, Babel, tsc)
- CI/CD pipeline: GitHub Actions, Jenkins, CircleCI, GitLab CI
- Container base images:
FROM node:18brings in an entire OS - Package registries: npm, PyPI, Maven Central, crates.io
- Development tools: IDE extensions, linters, formatters
- Third-party services: APIs your application calls at runtime
Each layer can be compromised. SolarWinds was a build tool attack. Codecov was a CI/CD attack. event-stream was a dependency attack.
Dependency Scanning and Management
Software Composition Analysis (SCA)
SCA tools identify dependencies and check them against vulnerability databases. Think of them as automated security research.
Free/built-in options:
# npm (built-in)
npm audit
npm audit fix # Automatically update to patched versions
# yarn
yarn audit
# Python
pip install pip-audit
pip-audit
# Go
go install github.com/sonatype-nexus-community/nancy@latest
go list -json -m all | nancy sleuth
# Rust
cargo install cargo-audit
cargo audit
# Java (Maven)
mvn dependency-check:check
# .NET
dotnet list package --vulnerable
Commercial/freemium SCA tools:
- Snyk: Excellent developer experience, IDE integration, fix PRs
- Dependabot: Free for GitHub repositories, auto-creates PRs
- GitHub Advanced Security: Dependency scanning, secret scanning, code scanning
- Mend (WhiteSource): Enterprise-focused, license compliance
- Sonatype Nexus IQ: Java ecosystem strength
- JFrog Xray: Integrates with Artifactory
Vulnerability Databases
Tools check against databases of known vulnerabilities:
- CVE (Common Vulnerabilities and Exposures): Industry standard identifiers
- NVD (National Vulnerability Database): NIST-maintained, includes CVSS scores
- GitHub Advisory Database: Open source, community-driven
- OSV (Open Source Vulnerabilities): Database designed for automation
A vulnerability report looks like this:
┌───────────────┬──────────────────────────────────────────────────────┐
│ high │ Regular Expression Denial of Service │
├───────────────┼──────────────────────────────────────────────────────┤
│ Package │ minimist │
├───────────────┼──────────────────────────────────────────────────────┤
│ Dependency of │ express [dev] │
├───────────────┼──────────────────────────────────────────────────────┤
│ Path │ express > minimist │
├───────────────┼──────────────────────────────────────────────────────┤
│ More info │ https://github.com/advisories/GHSA-xvch-5gv4-984h │
└───────────────┴──────────────────────────────────────────────────────┘
Automated Pull Requests
Configure tools to automatically create PRs when updates are available:
Dependabot configuration (.github/dependabot.yml):
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 10
reviewers:
- "security-team"
labels:
- "dependencies"
- "security"
- package-ecosystem: "docker"
directory: "/"
schedule:
interval: "weekly"
Renovate configuration (more customizable):
{
"extends": ["config:base"],
"vulnerabilityAlerts": {
"enabled": true,
"labels": ["security"]
},
"packageRules": [
{
"matchUpdateTypes": ["minor", "patch"],
"automerge": true
}
]
}
Triaging Vulnerabilities
You’ll get many alerts. Not all are critical. Prioritize based on:
- Severity: Critical > High > Medium > Low
- Exploitability: Is there a known exploit? (EPSS score helps)
- Exposure: Is the vulnerable code actually reachable in your application?
- Network exposure: Internet-facing vs internal service
- Fix availability: Is there a patched version?
The triage process:
1. Critical + Exploit exists + Publicly exposed → Fix immediately
2. High + No known exploit + Patch available → Fix within a week
3. Medium + Transitive dependency + Limited exposure → Fix in next sprint
4. Low + Dev dependency only → Schedule for next maintenance window
Don’t ignore vulnerabilities, but don’t panic over every medium-severity alert in a dev-only dependency either.
Software Bill of Materials (SBOM)
An SBOM is a complete inventory of all components in your software. Think of it as an ingredient list.
Why SBOMs Matter
When Log4Shell was announced in December 2021, the first question every organization asked was: “Do we use Log4j anywhere?” Organizations with SBOMs could answer in hours. Others spent weeks searching.
SBOMs enable:
- Vulnerability impact analysis: “Which applications are affected by CVE-2024-XXXX?”
- License compliance: “Are we using any GPL-licensed code?”
- Incident response: “Did the compromised package get into production?”
- Regulatory compliance: Increasingly required by government contracts
SBOM Formats
Two main standards:
SPDX (Software Package Data Exchange):
- ISO standard (ISO/IEC 5962:2021)
- Developed by Linux Foundation
- Focus on license compliance
- Human-readable and machine-readable formats
CycloneDX:
- OASIS standard
- Focus on security use cases
- Lightweight, designed for automation
- Includes vulnerability data (VEX - Vulnerability Exploitability eXchange)
Both work. CycloneDX is gaining momentum in security contexts. SPDX has stronger ecosystem for license compliance.
Generating SBOMs
For Node.js:
# Install CycloneDX
npm install -g @cyclonedx/cyclonedx-npm
# Generate SBOM
cyclonedx-npm --output-file sbom.json
# Or as SPDX
npm install -g @microsoft/sbom-tool
sbom-tool generate -b . -bc . -pn MyApp -pv 1.0.0
For Python:
# CycloneDX
pip install cyclonedx-bom
cyclonedx-py -o sbom.json
# SPDX
pip install spdx-tools
spdx-tools generate -f json -o sbom.spdx.json
For Go:
# Use Syft (supports multiple formats)
brew install syft
syft packages . -o cyclonedx-json > sbom.json
For containers:
# Syft works great for container images
syft packages nginx:latest -o cyclonedx-json
# Or Trivy
trivy image --format cyclonedx nginx:latest
When SBOMs Are Required
- US Federal government contracts: Executive Order 14028 (2021) requires SBOMs
- Critical infrastructure: NTIA minimum elements for SBOM
- Regulated industries: Healthcare, finance increasing requirements
- Enterprise procurement: Large companies requesting SBOMs from vendors
Even if not required, generating SBOMs as part of your build process gives you the inventory you’ll need when the next Log4Shell happens.
Dependency Review Process
Not every package needs deep review, but high-impact dependencies deserve scrutiny.
What to Review
Before installing a new dependency:
-
Popularity and maintenance:
npm info <package-name>Look for:
- Weekly downloads (more = more scrutiny by community)
- Last publish date (abandoned packages are risky)
- Number of maintainers (single maintainer = single point of failure)
-
Repository health:
- Active development or abandoned?
- How are issues handled?
- Recent commits
- Number of contributors (diversity = resilience)
-
Security posture:
- Check GitHub Security tab for advisories
- Search CVE databases:
site:cve.mitre.org <package-name> - Look for a SECURITY.md file (shows security is taken seriously)
-
License compatibility:
- What license does it use?
- Is it compatible with your application’s license?
- Are there copyleft requirements?
-
Bundle size (for frontend):
- Check bundlephobia.com
- Does it pull in unnecessary dependencies?
-
Alternatives:
- Is there a more established alternative?
- Could you implement it yourself in 100 lines?
Red Flags
- Recently transferred ownership
- Single maintainer with no activity
- Unusual permissions requested
- Large number of dependencies for simple functionality
- Package name similar to popular package (typosquatting)
- No repository link
- Recent burst of activity after long dormancy
Trust your instincts. If something feels off, investigate further or choose an alternative.
Lock Files and Reproducible Builds
Why Lock Files Matter
Without lock files:
{
"dependencies": {
"express": "^4.18.0"
}
}
The ^ means “any version 4.x.x”. Today you might get 4.18.2. Tomorrow someone could publish 4.18.3 with malicious code. Every npm install could pull different versions.
With lock files (package-lock.json):
- Exact version of every package is recorded
- Transitive dependencies are pinned
- Builds are reproducible
- Security: attackers can’t inject malicious versions without modifying lock file
Lock File Types
| Language | Lock File | Commit It? |
|---|---|---|
| Node.js (npm) | package-lock.json | Yes |
| Node.js (yarn) | yarn.lock | Yes |
| Node.js (pnpm) | pnpm-lock.yaml | Yes |
| Python (pip) | requirements.txt (not true lock) | Yes |
| Python (pipenv) | Pipfile.lock | Yes |
| Python (poetry) | poetry.lock | Yes |
| Go | go.sum | Yes |
| Rust | Cargo.lock | Yes (for apps), No (for libraries) |
| Ruby | Gemfile.lock | Yes |
| PHP | composer.lock | Yes |
| Java (Maven) | None (use version ranges carefully) | N/A |
| Java (Gradle) | gradle.lockfile (opt-in) | Yes |
Always commit lock files for applications. For libraries, it depends (Rust libraries don’t commit Cargo.lock to allow flexibility for users).
Dependency Pinning Strategies
Exact pinning:
{
"dependencies": {
"express": "4.18.2" // Exact version only
}
}
Maximum security, maximum maintenance. You control every update.
Semver ranges:
{
"dependencies": {
"express": "^4.18.2" // Allows 4.x.x updates
}
}
Automatic patch and minor updates. Assumes maintainers follow semver correctly (they don’t always).
The pragmatic approach:
- Use ranges in
package.jsonto accept patches - Lock file pins exact versions
- Automated tools (Dependabot) create PRs for updates
- CI tests changes before merging
- You control when updates land in production
License Compliance
Dependencies don’t just bring functionality - they bring legal obligations.
Open Source License Types
Permissive licenses (few restrictions):
- MIT: Do whatever you want, just include the license notice
- Apache 2.0: Like MIT, plus patent protection
- BSD: Similar to MIT, slight variations (2-clause, 3-clause)
Copyleft licenses (share-alike requirements):
- GPL v2/v3: If you distribute, you must share source code under GPL
- LGPL: Like GPL but allows linking without full GPL requirements
- AGPL: Like GPL but also triggered by network use (SaaS)
Other:
- Mozilla Public License (MPL): File-level copyleft (middle ground)
- Creative Commons: For content, not software
- Unlicense / Public Domain: No restrictions
Why This Matters
If you use a GPL library in your proprietary application and distribute it, you might be required to open-source your entire application. Companies have been sued over this.
AGPL is particularly tricky - offering your application as a web service counts as “distribution”, triggering the share-alike requirement.
License Scanning Tools
# npm
npm install -g license-checker
license-checker --summary
# Python
pip install pip-licenses
pip-licenses
# Go
go install github.com/google/go-licenses@latest
go-licenses report ./...
# Multi-language
npm install -g license-report
license-report --output=table
Creating a License Policy
Define what’s acceptable:
APPROVED:
- MIT, Apache 2.0, BSD (2-clause, 3-clause)
- ISC, Unlicense
REVIEW REQUIRED:
- LGPL, MPL
PROHIBITED:
- GPL, AGPL (for proprietary software)
- No license / All Rights Reserved
- Custom licenses (require legal review)
Enforce in CI:
license-checker --failOn "GPL;AGPL"
Typosquatting and Malicious Packages
Attackers register package names similar to popular packages, hoping for developer typos.
Attack Patterns
Typosquatting:
reqeustsinstead ofrequestselectorninstead ofelectronbabelcliinstead ofbabel-cli
Combosquatting:
lodash-utils(lodash doesn’t publish this)react-native-core(official packages don’t use this naming)
Brandjacking:
- Using brand names:
stripe,paypal,awsas prefixes
Real incidents:
- 2017:
crossenvtyposquattingcross-env(stole environment variables) - 2021:
ua-parser-js,coa,rccompromised (cryptocurrency miners) - 2022: 200+ malicious Python packages removed from PyPI
Defenses
- Copy-paste package names from official documentation
- Check package page before installing:
npm info <package-name> - Look for verified publishers (npm has verified badges)
- Use private registries for internal packages (prevents confusion attacks)
- Watch for warnings:
npm WARN notice Package "my-package" differs from official "my-package"
Container Security
Containers bundle your code with dependencies and base OS. The supply chain includes everything in that image.
Base Image Selection
Official images vs custom:
# Official Node.js image - maintained by Docker/Node team
FROM node:18-alpine
# Random image from Docker Hub - who maintains it?
FROM someuser/node-custom:latest # Risky
Use official images when possible. Check when they were last updated.
Full vs minimal vs distroless:
# Full Debian base (400+ MB, many packages, larger attack surface)
FROM node:18
# Alpine Linux (50MB, minimal packages)
FROM node:18-alpine
# Distroless (smallest, no shell, no package manager)
FROM gcr.io/distroless/nodejs18
Alpine and distroless reduce attack surface. Trade-off: debugging is harder without a shell.
Dockerfile Best Practices
# Pin exact base image version (not "latest")
FROM node:18.17.1-alpine3.18
# Create non-root user
RUN addgroup -g 1001 -S nodejs && adduser -S nodejs -u 1001
# Set working directory
WORKDIR /app
# Copy package files and lock file
COPY package*.json ./
# Install dependencies (production only)
RUN npm ci --omit=dev
# Copy application code
COPY . .
# Switch to non-root user
USER nodejs
# Expose port
EXPOSE 3000
# Run application
CMD ["node", "server.js"]
Key points:
- Pin exact versions
- Use
npm ci(respects lock file exactly) - Don’t run as root
- Use
.dockerignoreto avoid copying secrets - Multi-stage builds to exclude build tools from final image
Image Scanning
Scan images for vulnerabilities:
# Trivy (free, fast, accurate)
brew install trivy
trivy image nginx:latest
# Grype (from Anchore)
brew install grype
grype nginx:latest
# Snyk
snyk container test nginx:latest
# Docker Scout (Docker's built-in)
docker scout cves nginx:latest
Scan in CI before pushing to registry:
- name: Scan image
run: |
trivy image --severity HIGH,CRITICAL --exit-code 1 myapp:${{ github.sha }}
Multi-stage Builds
Reduce attack surface by excluding build tools from production image:
# Build stage
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Production stage
FROM node:18-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
USER node
CMD ["node", "dist/server.js"]
The final image doesn’t include source code, development dependencies, or build tools.
CI/CD Security
Your CI/CD pipeline has access to source code, secrets, and production. Compromising it means compromising everything.
Secure Build Pipelines
Principle of least privilege:
# GitHub Actions - minimal permissions
permissions:
contents: read
pull-requests: write
# Don't give "write: all" unless necessary
Pin third-party actions:
# Bad - uses whatever the current version is
- uses: actions/checkout@v4
# Better - pinned to specific version
- uses: actions/checkout@v4.1.0
# Best - pinned to commit SHA (immutable)
- uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608
Version tags can be moved to point at malicious code. Commit SHAs cannot.
Secret Management in CI/CD
Never hardcode secrets:
# Bad
env:
DATABASE_URL: "postgresql://user:password@host/db"
# Good - use secrets management
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
Use CI/CD provider’s secret storage (GitHub Secrets, GitLab CI/CD variables, CircleCI contexts).
For production deployments, use OIDC for keyless authentication:
# GitHub Actions OIDC to AWS
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/GitHubActions
aws-region: us-east-1
# No long-lived AWS keys needed
Third-Party Action Security
GitHub Actions and CircleCI Orbs run arbitrary code in your build environment. They can:
- Read source code
- Access secrets
- Modify builds
- Exfiltrate data
Codecov breach (2021): Bash Uploader script was compromised, stealing environment variables from thousands of CI builds.
Mitigations:
- Review third-party actions before using
- Pin to commit SHA
- Use only actions from verified publishers
- Consider self-hosting runners for sensitive workloads
- Audit what actions have access to secrets
Common Pitfalls
-
“We’ll update dependencies later”: Later never comes. Vulnerabilities accumulate. Update regularly.
-
Ignoring dev dependencies: Dev dependencies can compromise developer machines or CI. They matter.
-
No SBOM until required: Generating SBOMs takes minutes. Answering “are we affected?” without one takes weeks.
-
Trusting package names: Always verify. Typosquatting is real and ongoing.
-
Not reading Dependabot PRs: Auto-merge isn’t always safe, but ignoring PRs indefinitely isn’t either. Review and merge promptly.
-
Running builds as root: If a compromised dependency runs during build, it has root access. Use least privilege.
-
No license compliance: Legal problems are expensive. Check licenses before legal gets involved.
-
Assuming official = safe: Even official packages have vulnerabilities. Scan everything.
-
Lock files not committed: Defeats the purpose. Commit them.
-
No incident response plan for supply chain: When a dependency is compromised, can you identify impact in hours?
Dependency Security Checklist
Development
- Lock files committed to version control
- Dependencies reviewed before installation
- License policy defined and documented
- Package names verified (not typosquatted)
- Minimal dependency footprint (avoid unnecessary packages)
Automation
- Vulnerability scanning in CI (
npm audit, Snyk, etc.) - Automated dependency update PRs (Dependabot, Renovate)
- Image scanning for containers
- SBOM generation in build pipeline
- License scanning in CI
Process
- Security alerts reviewed weekly
- Critical vulnerabilities fixed within 72 hours
- Dependency updates tested before production
- Incident response plan includes supply chain scenarios
- Regular dependency updates (at least monthly)
Container-Specific
- Base images pinned to specific versions
- Official images used when possible
- Images scanned before pushing to registry
- Multi-stage builds to minimize final image
- Containers run as non-root users
CI/CD
- Third-party actions pinned to commit SHA
- Secrets stored in CI provider secret management
- Least privilege permissions on CI jobs
- Build artifacts signed or attested
- CI environment isolated from production