Your application is an iceberg. The code you wrote is the tip — 10 to 20% of what actually runs in production. The rest is frameworks, libraries, utilities, and transitive dependencies that you pulled from public registries with a one-line install command. You probably haven’t read the source code of most of them. You probably don’t know who maintains them. And you definitely haven’t checked whether the maintainer’s npm account has MFA enabled.
The event-stream attack, the ua-parser-js hijack, the Log4Shell vulnerability — every major supply chain incident traces back to a dependency that somebody trusted without verifying. This guide is the practical checklist for managing that trust: lock files, audits, SBOMs, automated updates, and the operational hygiene that keeps your dependency tree from becoming your biggest liability.
DO / DON’T
DO:
- Commit lock files to your repository — always
- Run dependency audits in CI on every build
- Enable automated dependency update tools (Dependabot, Renovate)
- Generate an SBOM for every release
- Review new dependencies before adding them (maintainers, download count, last update, license)
- Pin exact versions for production deployments
- Use a private registry or registry proxy for critical projects
DON’T:
- Run
npm installorpip installwithout checking the lock file diff - Add a dependency for something you can write in 10 lines of code
- Ignore
npm auditorpip auditoutput - Use
*orlatestas version specifiers in production - Trust a package solely because it has high download numbers — popularity is not security
- Leave unused dependencies in your project — every dependency is attack surface
- Skip auditing transitive dependencies — they’re the ones you don’t see
Lock Files
Lock files are the single most important dependency security control. They pin the exact version and integrity hash of every dependency — direct and transitive — so that every build uses exactly the same code.
What to Do
- Commit your lock file.
package-lock.json,yarn.lock,pnpm-lock.yaml,Pipfile.lock,poetry.lock,Cargo.lock,go.sum,Gemfile.lock— whatever your ecosystem uses, it goes in version control. If it’s not committed, every build might resolve to different versions. - Review lock file diffs. When a dependency update changes the lock file, review the diff. New packages appearing, version jumps, and integrity hash changes are all worth examining. A lock file diff that adds unexpected new packages could indicate a dependency confusion attack.
- Use
ciorfrozeninstall modes.npm ci(notnpm install) installs exactly what’s in the lock file and fails if the lock file is out of sync withpackage.json.pip install --require-hashesverifies download integrity. These modes prevent silent dependency resolution changes during builds.
# Node.js — use npm ci in CI/CD
npm ci
# Python — install with hash verification
pip install --require-hashes -r requirements.txt
# Ruby — use frozen mode
bundle install --frozen
Integrity Verification
Lock files with integrity hashes (npm’s sha512 in package-lock.json, pip’s --hash mode) ensure that the downloaded package matches what was originally resolved. If someone tampers with a package on the registry, the hash check fails and the build stops. This is your defense against registry compromise.
Dependency Auditing
Auditing checks your dependency tree against known vulnerability databases (the National Vulnerability Database, GitHub Advisory Database, OSV).
Automated Audits in CI
Run audits on every build. Block on critical and high vulnerabilities:
# Node.js
npm audit --audit-level=high
# Python
pip audit
# Ruby
bundle audit check --update
# Go
govulncheck ./...
# Rust
cargo audit
# Java/Kotlin (using OWASP Dependency-Check)
mvn org.owasp:dependency-check-maven:check
Triage and Remediation
Not every audit finding is actionable immediately:
- Critical with known exploit: Fix immediately. This is being actively exploited. Check the CISA KEV catalog.
- High without known exploit: Fix within days. Update the dependency or find an alternative.
- Medium/Low in transitive dependencies: Evaluate whether the vulnerable code path is reachable in your application. If it is, update. If it isn’t, document the risk acceptance and set a review date.
- No fix available: Evaluate alternatives. If the dependency is unmaintained and has known vulnerabilities, it’s time to replace it — not wait for a fix that won’t come.
Automated Dependency Updates
Manual dependency updates don’t happen. Automated ones do.
Dependabot (GitHub)
Enable in repository settings under “Code security and analysis.” Configure in .github/dependabot.yml:
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 10
labels:
- "dependencies"
- "security"
Dependabot creates PRs for outdated and vulnerable dependencies. If your tests pass, merge with confidence. Security updates get priority PRs automatically.
Renovate
More configurable than Dependabot. Supports grouping minor/patch updates into a single PR, auto-merging low-risk updates, and scheduling update windows:
{
"extends": ["config:base"],
"automerge": true,
"automergeType": "pr",
"major": {
"automerge": false
},
"vulnerabilityAlerts": {
"enabled": true,
"labels": ["security"]
}
}
Auto-merge minor and patch updates when tests pass. Require manual review for major version bumps (breaking changes). Always require manual review for security-flagged updates — read the advisory before merging.
SBOM Generation
A Software Bill of Materials (SBOM) lists every component in your software. When the next Log4Shell-scale vulnerability drops, an SBOM tells you in minutes whether you’re affected — instead of the days or weeks it takes to manually audit every project.
Generating SBOMs
# Using Syft (supports many ecosystems)
syft . -o cyclonedx-json > sbom.json
# Using CycloneDX (Node.js)
npx @cyclonedx/cyclonedx-npm --output-file sbom.json
# Using CycloneDX (Python)
cyclonedx-py requirements > sbom.json
Generate an SBOM for every release. Store it alongside the release artifacts. The two standard formats are CycloneDX (OWASP-backed, security-focused) and SPDX (Linux Foundation-backed, license-focused). Either works. Pick one and be consistent.
Executive Order 14028 requires SBOMs for software sold to the US federal government. Even if you don’t sell to the government, an SBOM is the fastest way to answer “are we affected?” when the next critical CVE drops.
Private Registries
For critical projects, proxying public registries through a private registry adds a layer of control and auditability.
Benefits
- Namespace control. Private packages resolve from your registry, not the public one. This prevents dependency confusion attacks where an attacker registers your internal package name on the public registry.
- Vetting and approval. New packages must be approved before they’re available through the proxy. This creates a review gate for new dependencies.
- Audit trail. Every package download is logged. You know exactly what was installed, when, and by whom.
- Availability. If the public registry goes down (npm has had outages), your cached packages are still available.
Options
- Artifactory — Enterprise-grade, supports all major package ecosystems
- Nexus Repository — Open-source option with broad ecosystem support
- Verdaccio — Lightweight npm registry proxy, easy to self-host
- Cloud-native: AWS CodeArtifact, Azure Artifacts, GCP Artifact Registry
For smaller teams, scoping your private packages (e.g., @yourorg/package-name in npm) and configuring .npmrc to resolve the scope from your private registry while allowing unscoped packages from the public registry is the minimum viable protection against dependency confusion.
Version Pinning Strategy
- Direct dependencies: Pin exact versions in production (
1.2.3, not^1.2.3). Use the lock file to enforce this. Semver ranges are useful during development but dangerous in production — a minor version bump can introduce breaking changes or, worse, malicious code. - Transitive dependencies: Controlled by the lock file. Review transitive updates when they appear in lock file diffs.
- Development dependencies: Can use semver ranges since they don’t ship to production. Still audit them — a compromised dev dependency runs on your build server.
If It Already Happened
If you’ve installed a compromised dependency, treat it as a full compromise of the environment where it ran. The malicious code executed with whatever permissions your install process, build system, or application had.
Immediate steps:
- Identify the compromised package and version. Check GitHub Security Advisories and OpenSSF’s package analysis for details on what the malicious code does.
- Remove the package. Pin to a known-good version or find an alternative.
- Rotate all secrets accessible from the compromised environment — CI/CD tokens, deployment credentials, API keys, cloud provider keys.
- Audit systems deployed by the compromised build for unauthorized changes, unexpected network connections, or additional malicious payloads.
- Check if the malicious code established persistence — reverse shells, cron jobs, modified startup scripts.
Report the package to the registry (npm, PyPI, RubyGems all have abuse reporting). Report to CISA if the compromise affects critical infrastructure.
Your dependencies are your code. You’re responsible for what they do, even though you didn’t write them. Lock them down. Audit them. Update them. Monitor them. The usual suspects know that the supply chain is the softest target in modern software — make sure your link in the chain holds.