Overview
Static vulnerability scanners report a CVE for every declared dependency that matches a vulnerable version, regardless of whether the vulnerable code is ever executed. A critical-severity finding in a test-only helper that your production build tree never reaches is a different risk level than the same CVE in a hot-path dependency. This skill operationalizes a reachability heuristic: run an ecosystem-native dead-dependency tool, produce an unused-deps.txt artifact, and feed that list into your SCA prioritization step so it can assign reachable: false and route affected findings to Fix-Backlog instead of Fix-Now.
The approach is static-analysis heuristic only. A dependency flagged unused by these tools is a strong signal to deprioritize its CVEs, not a guarantee that the vulnerable code is unreachable at runtime. Treat it accordingly.
When to use
How to use
Cross-reference unused deps with SCA findings
Once you have an unused-deps.txt for your ecosystem, annotate the SCA JSON output before passing it to your SCA prioritization step:
import json
with open("osv.json") as f:
findings = json.load(f)
with open("unused-deps.txt") as f:
unused = {line.strip() for line in f}
for finding in findings.get("results", []):
pkg = finding.get("package", {}).get("name", "")
finding["reachable"] = pkg not in unused
with open("osv-annotated.json", "w") as f:
json.dump(findings, f, indent=2)
Pass osv-annotated.json to your SCA prioritization step. Per the priority logic, findings with reachable: false route to Fix-Backlog regardless of severity, unless they are in the CISA KEV catalog.
CI integration
jobs:
reachability:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: JS unused deps (knip)
run: npx knip --reporter json | jq -r '.dependencies[].name' > unused-deps.txt
- name: Annotate OSV output
run: python3 ci/annotate-reachability.py osv.json unused-deps.txt
- uses: actions/upload-artifact@v4
with:
name: osv-annotated
path: osv-annotated.json
The annotated artifact is then consumed by the prioritization job.
Worked example
A team's OSV scan of a Next.js service returns 60 CVEs - too many to patch in one sprint. They run the JS dead-dependency tool (step 2), which flags 12 of the 80 declared dependencies as unused, including moment (CVE-2022-31129, CVSS 7.5). The cross-reference script sets reachable: false on the moment finding because moment appears in unused-deps.txt. Prioritization routes that CVE to Fix-Backlog instead of Fix-This-Sprint, and the team removes moment in the next dependency-cleanup PR, eliminating both the CVE and the unused weight in one change.
Anti-patterns
| Anti-pattern | Why it fails | Fix |
|---|---|---|
| Skip reachability; sort SCA output by CVSS only | Teams spend sprints patching vulns in unused code while exploitable issues sit in Fix-Backlog | Run the cross-reference step before triage |
Treat reachable: false as safe | Static heuristic; dynamic imports and plugin systems can load code at runtime | Use as deprioritization signal only; still schedule Fix-Backlog cleanup |
| Run vulture at default confidence (60%) | Reports unused functions/variables alongside imports; noisy for CVE mapping | Use --min-confidence 90 to target imports specifically |
| Use depcheck on new JS projects | Archived June 2025; per github.com/depcheck/depcheck (opens in new window), maintainers recommend knip | Switch to knip for new setups |
Skip --with-metadata in Rust monorepos | Renamed crates not detected; false "used" verdicts | Always pass --with-metadata in CI |
| Ignore devDependencies entirely | Dev deps with CVEs can reach production in bundled builds | Separate prod/dev lists; confirm --omit=dev in audit scope |
Limitations
References
Per-ecosystem detail for producing unused-deps.txt: config files, flags, exit codes, and false-positive suppression. Run the tool for your ecosystem, then extract the unused names for the SCA cross-reference step.
JavaScript / TypeScript: knip (preferred) or depcheck
knip is the current recommended tool for unused-dependency detection in JS projects. Per github.com/webpro-nl/knip (opens in new window), it detects unused files, exports, and dependencies including dev-only packages.
npx knip --reporter json > knip-report.json
# Extract unused dependency names for the cross-reference list
npx knip --reporter json | jq -r '.dependencies[].name' > unused-deps.txt
depcheck remains usable for existing setups. Per github.com/depcheck/depcheck (opens in new window), the project was archived in June 2025; the maintainers recommend switching to knip for new setups. For teams still on depcheck:
npx depcheck --json > depcheck-report.json
# Extract unused package names
jq -r '.dependencies[],.devDependencies[]' depcheck-report.json > unused-deps.txt
Per github.com/depcheck/depcheck (opens in new window), the --json flag produces an object with dependencies (unused production deps) and devDependencies (unused dev deps) as arrays of package-name strings. Packages in devDependencies that never appear in the production dep tree are prime candidates for reachable: false annotation on any CVEs they carry.
Suppress known false positives via .depcheckrc:
# .depcheckrc
ignores: ["babel-register", "eslint-*"]
ignore-patterns: ["dist", "coverage"]
Scope narrowing: dev vs production
Dev dependencies in non-production scope are already excluded from many scanners. Per github.com/depcheck/depcheck (opens in new window), running npm audit --omit=dev skips devDependencies entirely. Always separate production and dev unused-dep lists:
# Separate production unused from dev unused
jq -r '.dependencies[]' depcheck-report.json > unused-prod.txt
jq -r '.devDependencies[]' depcheck-report.json > unused-dev.txt
Python: vulture
Per github.com/jendrikseipp/vulture (opens in new window), vulture detects unused imports, functions, classes, and variables through static analysis. For dependency reachability, unused imports are the direct signal.
pip install vulture
vulture src/ --min-confidence 90 > vulture-report.txt
Per github.com/jendrikseipp/vulture (opens in new window), --min-confidence 90 targets imports specifically (confidence 90% for unused imports). Lower values include functions and variables, which is noisier for the dep-CVE cross-reference task.
Extract the unused-import lines:
grep "unused import" vulture-report.txt | sed "s/:.*unused import '\(.*\)'.*/\1/" > unused-imports.txt
Map import names back to pip package names using pip show or a manually maintained import-to-package.txt map, since Python import names often differ from PyPI package names (e.g. import PIL comes from Pillow).
Configure via pyproject.toml per github.com/jendrikseipp/vulture (opens in new window):
[tool.vulture]
paths = ["src/"]
min_confidence = 90
exclude = ["tests/", "migrations/"]
ignore_names = ["celery_app", "urlpatterns"]
Suppress known false positives (e.g. plugin registrations, dynamic imports) by generating a whitelist:
vulture src/ --make-whitelist > whitelist.py
# Then re-run including the whitelist
vulture src/ whitelist.py --min-confidence 90
Per github.com/jendrikseipp/vulture (opens in new window), exit code 3 means dead code found; 0 means clean.
Rust: cargo-machete
Per github.com/bnjbvr/cargo-machete (opens in new window), cargo-machete detects unused [dependencies] in Cargo.toml through fast static analysis.
cargo install cargo-machete
cargo machete
Per github.com/bnjbvr/cargo-machete (opens in new window), exit code 0 means no unused dependencies found; exit code 1 means at least one unused dependency was detected; exit code 2 signals a processing error.
For more accurate detection when crates use renamed or feature-gated imports, use --with-metadata:
cargo machete --with-metadata
Per github.com/bnjbvr/cargo-machete (opens in new window), --with-metadata calls cargo metadata --all-features to resolve final dependency names, which catches renames that simple text search misses.
Suppress false positives (e.g. proc-macro crates loaded at compile time only) via Cargo.toml metadata per github.com/bnjbvr/cargo-machete (opens in new window):
[package.metadata.cargo-machete]
ignored = ["prost", "openssl"]
[package.metadata.cargo-machete.renamed]
rustls-webpki = "webpki"
Capture the unused-dep list:
cargo machete 2>&1 | grep "unused dependency" | awk '{print $NF}' > unused-deps.txt