Why npm audit Is Broken
Run npm audit on any non-trivial project and you'll get a wall of advisories. Half are in dev dependencies you never ship. A third are transitive deps five levels deep that your code never imports. The rest might actually matter, but you can't tell which ones because npm treats every advisory the same: red text, scary words, no context.
I got tired of this. So I built auditfix β a CLI that replaces npm audit with actual signal.
The Core Problem
npm audit reports vulnerabilities by matching package names and versions against the GitHub Advisory Database. That's it. No reachability analysis. No exploit likelihood. No distinction between a critical RCE in your production Express server and a ReDoS in a dev-only linter's transitive dependency.
The result: alert fatigue. Teams either ignore npm audit entirely or waste hours triaging advisories that don't affect production.
Production Reachability: The 60% Noise Cut
The single most impactful feature in auditfix is production reachability analysis. The idea is simple: if a vulnerable package only exists in your devDependencies tree, it never reaches production, and it shouldn't block your CI pipeline.
For npm lockfiles, this is almost free. npm 9+ pre-computes dev, optional, and devOptional boolean flags on every entry in package-lock.json. If none of those flags are true, the package is production. No graph traversal needed β just an O(n) scan:
// npm lockfile: trust pre-computed flags
for (const [key, entry] of Object.entries(packages)) {
const isProduction = !entry.dev && !entry.optional && !entry.devOptional;
graph.set(key, { ...parsed, isProduction });
}For Yarn and pnpm, there are no pre-computed flags. So auditfix builds the dependency graph from the lockfile and runs a BFS from production roots:
function markProductionReachable(
graph: DependencyGraph,
productionRoots: string[]
): void {
const visited = new Set<string>();
const queue = [...productionRoots];
while (queue.length > 0) {
const key = queue.shift()!;
if (visited.has(key)) continue;
visited.add(key);
const node = graph.get(key);
if (!node) continue;
node.isProduction = true;
for (const dep of node.dependencies) {
if (!visited.has(dep)) queue.push(dep);
}
}
}This alone cuts 60-70% of npm audit noise on typical projects. Dev-only vulnerabilities still get reported, but as low severity regardless of CVSS score.
Risk Scoring: Not All Vulnerabilities Are Equal
npm audit gives you severity labels from the advisory database (critical/high/medium/low). These come from CVSS base scores, which measure theoretical impact β not actual risk to your application.
auditfix computes a composite risk score (0-100) that factors in what actually matters:
score = 0
score += cvssScore * 4 // 0-40 points: base severity
score += isProduction ? 30 : 0 // 30 points: production exposure
score += isDirectlyImported ? 10 : 0 // 10 points: actually imported in src/
score += exploitScore // 0-20 points: EPSS + CISA KEV
score += !fixAvailable ? 5 : 0 // 5 points: no patch exists
score += directDependency ? 5 : 0 // 5 points: you control the version
score -= Math.min(depth, 5) // -1 to -5: deeper = less urgent
The key insight: critical in auditfix requires all three conditions β production reachable, exploit available, and CVSS >= 7. A CVSS 9.8 in a dev-only package is low. A CVSS 7.0 with a known exploit in your production Express server is critical.
EPSS: Probability Over Panic
CVSS tells you how bad a vulnerability could be if exploited. EPSS (Exploit Prediction Scoring System) tells you how likely it will be exploited in the next 30 days. It's a probability score from 0 to 1, maintained by FIRST.org.
auditfix fetches EPSS scores in real-time and uses graduated scoring:
function getExploitScore(cve: string, epss: number, isInKEV: boolean): number {
// CISA KEV = actively exploited right now
if (isInKEV) return 20;
// EPSS graduated scoring
if (epss >= 0.5) return 20; // Top 2% β active exploitation likely
if (epss >= 0.1) return 15; // Top 10%
if (epss >= 0.01) return 8; // Top 30%
return 2; // Data exists, low likelihood
}CISA's Known Exploited Vulnerabilities (KEV) catalog is the nuclear option β if a CVE is in KEV, it's being actively exploited in the wild. That's an automatic 20 points regardless of EPSS.
Advisory Fetching: Three-Tier Fallback
auditfix doesn't rely on a single data source. It uses a three-tier fallback chain:
- OSV.dev batch API (primary) β real-time, no auth required, batch queries up to 1,000 packages per request
- Local cache β
~/.auditfix/cache/with 24-hour TTL, written in background after successful OSV fetch - Bundled offline index β shipped with the npm package, works with zero network
The OSV batch endpoint returns abbreviated results (just IDs), so auditfix follows up with parallel detail fetches (bounded concurrency of 10) for full CVSS vectors and fix versions.
One detail that cost me hours: OSV version ranges use an event-based format that needs conversion to semver ranges. And the semver matching must use includePrerelease: true:
semver.satisfies(version, range, { includePrerelease: true })Without that flag, prerelease versions like 1.0.5-beta.1 silently pass through vulnerability checks for ranges like <1.0.5. That's a false negative on a vulnerable package. The kind of bug that makes security tools worse than useless.
Supply Chain: Beyond CVEs
Vulnerabilities aren't the only threat. auditfix includes supply chain scanning that catches threats CVE databases don't cover:
Typosquatting detection β Levenshtein distance against 100+ popular packages. Catches attacks like lodahs (lodash), exprss (express), or scope-stripping like types-node (@types/node).
Install script scanning β Reads preinstall, install, and postinstall scripts from every package. Flags suspicious patterns: HTTP downloads, eval(), child_process spawning, env var harvesting. These are the exact vectors used in real supply chain attacks (ua-parser-js, event-stream, node-ipc).
Provenance verification β Checks Sigstore attestations on npm packages. Flags production dependencies without provenance, which means you can't verify the published package matches the source repo.
Behavioral analysis β Deep scans package source code for patterns like bulk process.env access, long hex strings (potential C2 addresses), filesystem access to sensitive paths, and dynamic code execution.
Safe Auto-Fix
npm audit fix is a blunt instrument. auditfix's --fix flag is surgical:
For direct dependencies, it checks if the fix version satisfies the declared semver range in package.json. If yes, it's a safe lockfile-only update. If no, it flags it as a breaking change requiring manual review.
For transitive dependencies, it uses the package manager's override mechanism (overrides for npm, resolutions for yarn, pnpm.overrides for pnpm) to force a specific version in the lockfile without touching package.json.
All shell commands use execFile with argument arrays β never string interpolation. Because a security tool with command injection would be embarrassing.
Output Formats
Terminal output is the default, but auditfix also generates:
- SARIF β for GitHub Code Scanning integration (upload via
github/codeql-action/upload-sarif) - CycloneDX SBOM β software bill of materials
- OpenVEX β machine-readable vulnerability exploitability statements
- JSON β for scripting and CI pipelines
The Result
auditfix v2.0.0 β scanned 847 packages
CRITICAL (1)
lodash@4.17.20 β Prototype Pollution
Production: YES | EPSS: 0.87 (18/20) | Fix: 4.17.21
HIGH (2)
express@4.17.1 β Open redirect
Production: YES | EPSS: 0.34 (7/20) | Fix: 4.19.2
LOW (1)
semver@5.7.1 β ReDoS
Production: NO (dev only) | EPSS: 0.02 (0/20)
β Low risk. Dev tooling only.
Summary: 1 critical | 2 high | 1 low (dev-only)
CI exit code: 1 (production vulnerabilities found)
Same project, npm audit reports 47 advisories. auditfix reports 4 β and tells you exactly which ones to fix first and why.
The tool is open source on GitHub and published on npm. 51 source files, 39 test files, 7 production dependencies. No arborist. No bloat.