Dependency / Supply-Chain Security
The supply chain is the part of your stack you trust by default and rarely review. npm, PyPI, RubyGems, Maven, Go modules — they have all hosted malicious packages, typosquats, and compromised maintainer accounts at scale.
This skill is the working developer's defense — pragmatic checks that fit in a normal workflow, not a software bill-of-materials regulatory program.
When to invoke
- Adding a new dependency to a project (especially a small / new / single-maintainer one)
- After a publicized supply-chain incident affecting your ecosystem
- Quarterly audit of
package.json/requirements.txt/go.mod - Inheriting a project with a large
node_modulesand no obvious hygiene - CI/CD just leaked a secret — start here to find which dep grabbed it
The threat model
What can a malicious dependency actually do?
- Read environment variables / files during install (postinstall script)
- Read environment variables / files at runtime (any code path)
- Exfiltrate via outbound HTTPS to any host
- Modify other dependencies (link-stage tampering, build-script monkey-patching)
- Hide payloads in transitive deps so your
package.jsonlooks clean
You cannot inspect every line of every dependency. You can raise the cost of attacks and detect them when they happen.
Lockfile hygiene
A lockfile is the only thing standing between "I added one dep" and "my install grabbed 1,200 transitive deps any of which can be swapped tomorrow".
- Commit the lockfile. Always:
package-lock.json,pnpm-lock.yaml,yarn.lock,poetry.lock,Pipfile.lock,go.sum. No exceptions. npm ci(orpnpm install --frozen-lockfile) in CI, notnpm install.installcan mutate the lockfile silently;cierrors if the lockfile andpackage.jsondisagree.- Lockfile drift alerts. A PR that touches
package-lock.jsonbut notpackage.json, or vice versa, deserves a review comment. Add a CI check. - Pin direct dependencies tightly when the dep is small / new / sketchy.
"left-pad": "1.3.0"rather than"left-pad": "^1.3.0". The caret invites unattended updates.
For high-stakes apps consider npm install --omit=dev in production builds — dev deps are a huge attack surface that production runtime does not need.
Built-in audit — useful, limited
npm audit --omit=dev --audit-level=high
pnpm audit --audit-level=high
yarn npm audit
pip-audit
What npm audit does well:
- Catches known CVEs against installed versions
- Fast, no extra tooling
- Good signal-to-noise at
--audit-level=highorcritical
What it misses:
- Malicious-by-design packages (the package is doing what it's "supposed to do", which happens to be evil)
- Unreported issues — many compromised packages are not in any CVE feed
- Behavior-level analysis —
auditdoesn't know that this package suddenly started making outbound HTTPS calls
Treat npm audit as the first line of defense, not the only one.
Behavior-level scanning
Tools that look at what a package does, not just which CVE it has:
- socket.dev — flags new packages, packages that gained network/install-script behavior, typosquat-shaped names. GitHub integration that comments on PRs. Free tier covers most personal use.
- deps.dev (Google) — package metadata, transitive graph, advisory feed
- OSV-Scanner — open-source scanner against the OSV database, broader than npm audit
- Snyk / GitHub Dependabot security alerts — overlap with audit but include curated advisories
A reasonable stack: GitHub Dependabot (free, native) + socket.dev (free, behavior-aware) + npm audit in CI.
Reviewing a new dependency before npm install
When adding a new dep, especially a small or unfamiliar one, take 60 seconds first.
# What is this package, who maintains it, what's the install footprint?
npm view <pkg>
npm view <pkg> dependencies # transitive count
npm view <pkg> repository.url # is there a public repo?
npm view <pkg> maintainers # who can publish a new version?
npm view <pkg> time # when was each version published?
# How popular is it? (huge install counts don't prove safety, but obscure single-digit downloads warrant scrutiny)
curl -s "https://api.npmjs.org/downloads/point/last-week/<pkg>" | jq
# Red flags in the publish history
npm view <pkg> time --json | jq 'to_entries | sort_by(.value) | reverse | .[0:5]'
Red flags:
- Single maintainer with no other notable packages + the package handles auth/crypto/payments
- Recently transferred ownership (new maintainer's other packages look unrelated)
- Sudden version jump with a vague changelog (
"bug fixes"and a 1k-line diff) - No public repo, no homepage, no README
- Typosquat-shaped name —
lodash-utilsvslodash,colors-jsvscolors - Postinstall script (see next section)
- Network calls during install (you can sandbox install to detect — see CI section)
Postinstall scripts
postinstall, preinstall, install, prepare scripts run on npm install. They are the most common malware delivery vector.
# Enumerate every package with an install-time script
find node_modules -name package.json -not -path '*/node_modules/*/node_modules/*' \
-exec grep -l '"postinstall"\|"preinstall"\|"prepare"' {} \; \
| xargs -I {} dirname {} | sed 's|node_modules/||'
# Block ALL install scripts globally — most packages do not need them
npm config set ignore-scripts true
# Or per-install
npm install --ignore-scripts
--ignore-scripts breaks some legitimate native-module builds (node-sass, sharp, better-sqlite3). Allow scripts only for the specific packages that need them — yarn 2+ and pnpm have allowlists.
In pnpm:
# .npmrc
ignore-scripts=true
# package.json
"pnpm": {
"onlyBuiltDependencies": ["sharp", "better-sqlite3"]
}
Typosquat detection
Common substitutions: i ↔ l ↔ 1, 0 ↔ o, missing/extra dashes, plural/singular. Run:
# Cheap typosquat heuristic — flag packages with very similar names to well-known ones
npm ls --depth=0 --json | jq -r '.dependencies | keys[]' | while read p; do
echo "$p" # human review or feed to socket.dev
done
socket.dev / npq do this systematically; for a manual pass, just eyeball your direct deps with cat package.json | jq '.dependencies | keys'.
CI / build sandbox
Don't let your install run with prod credentials sitting in env.
# GitHub Actions
- name: Install dependencies
run: npm ci --ignore-scripts
env: {} # no env vars passed in
# Then run the few allowed install scripts intentionally
- name: Run build scripts for native deps
run: npm rebuild sharp better-sqlite3
See github-actions-security for the broader CI story (pinned action SHAs, OIDC, secret scoping).
Audit node_modules for an inherited project
# How many packages did this single npm install actually install?
find node_modules -name package.json -not -path '*/node_modules/*/node_modules/*' | wc -l
# Largest packages — these are where you'd hide a payload
du -sh node_modules/*/ | sort -h | tail -20
# Packages with install scripts (as above)
# Packages with .bin executables
find node_modules/.bin -type l -o -type f | head -30
Cross-check against package.json direct deps. Anything in node_modules you cannot trace back to a direct or expected-transitive dep is worth investigating.
What to do when a dependency is compromised
You wake up to "package X version 2.4.5 was compromised, contains a credential stealer".
- Determine if you shipped it.
# Did you have it installed at the bad version? git log --all -p -- package-lock.json | grep -A2 -B2 'package-x.*2\.4\.5' - Assume credentials accessed during install/run are burned. That includes
.envfiles readable by the user that rannpm install, plus anything in process env at runtime. - Rotate every credential that was in scope. Order per
secret-hygiene. - Pin to a known-clean version, or remove the package entirely if no clean version exists.
- Audit
package-lock.jsonfor the transitive surface — the compromised package may be a transitive dep you didn't add directly. - Check CI logs for unexplained outbound traffic during recent builds.
- Add the package + version range to a permanent denylist in your CI (whatever scanner you use) so you don't accidentally re-add it.
Periodic audit checklist
Once a quarter:
- Lockfile present and committed for every project
- CI uses
npm ci/--frozen-lockfile, not loose install - Dependabot (or equivalent) enabled, PRs reviewed not auto-merged for major versions
-
npm audit --audit-level=highpasses; exceptions documented - socket.dev or equivalent reviews new dependency PRs
- Install scripts allowlisted; defaults to ignore
- No env vars passed to
npm cistep in CI - Direct dependencies you don't recognize have been investigated
- A known-vulnerable package list is maintained for the project's ecosystem
What this skill will not do
- Replace a formal Software Bill of Materials (SBOM) program for regulated environments
- Provide payloads or attack patterns to publish malicious packages
- Recommend installing packages without lockfiles "for now"