Silence the Noise: Why I Killed Dependabot (And Why You Should Consider It)
Silence the Noise: Why I Killed Dependabot (And Why You Should Consider It)
Introduction
It was 8:00 AM on a Monday, that time usually reserved for coffee and existential dread, not triage. My inbox was screaming. Forty-seven new emails. Was the production database on fire? Did the payment gateway collapse? No. It was Dependabot. Again.
Forty-seven pull requests warning me that a regular expression library used exclusively by my CSS minifier had a "Critical Severity" vulnerability. Apparently, if a hacker could somehow inject a malicious string into my build pipeline during the 30 seconds it runs on GitHub Actions, they could cause a ReDoS (Regular Expression Denial of Service).
I closed the tab. I archived the emails. And then I did something forbidden: I turned the bot off.
We need to talk about "Alert Fatigue" and the state of security theater in our industry. We are optimizing for green dashboards, not secure systems.
The Boy Who Cried "CVE"
Here is the uncomfortable truth: most automated security tools are dumb. They are grep-on-steroids that match version strings against a database of CVEs (Common Vulnerabilities and Exposures). They do not understand your architecture, they do not understand context, and they do not understand reachability.
Filippo Valsorda wrote a great article about govulncheck that points out the flaw in our mental model: we treat dependencies as monoliths. If a library has a vulnerability in function A(), but you only use function B(), are you actually vulnerable?
Logic says no. Dependabot says PANIC.
This noise is not harmless. It causes "Alert Fatigue." When you receive 100 warnings and 99 are false positives, you stop checking. Eventually, the one real vulnerability, the one that actually exposes your user data, slips through because it looked exactly like the 99 warnings about a dev-dependency regex parser.
The Go Standard vs. The Node.js Wild West
The Go ecosystem is years ahead here. Tools like govulncheck perform static analysis to see if your code actually calls the vulnerable function. It constructs a call graph. It is precise. It respects your time.
In JavaScript/TypeScript, we are still playing whack-a-mole. We rely on npm audit, which creates a blast radius so wide it hits dependencies of dependencies of a tool you only run locally.
While we wait for the JS ecosystem to reach proper reachability analysis (CodeQL is getting there, but it is heavy), we need a system to manage the noise. We cannot let the fear of "potential" vulnerabilities paralyze our actual development velocity.
Implementation: Building a Sanity Layer
Since we do not have a native govulncheck for every Node.js project yet, we have to build our own filter. We need a way to run security audits that respect context, filtering out noise from devDependencies or specific advisories we have deemed "Accepted Risks."
Stop letting npm audit break your CI pipeline for irrelevant issues. Let's write a TypeScript utility that acts as middleware between the raw audit data and your CI status check.
The Plan
- Run
npm audit(orpnpm audit) and output raw JSON. - Ingest that JSON into a custom script.
- Filter out vulnerabilities based on strict criteria (e.g., "Production dependencies only" or "Ignore Advisory X").
- Fail the build only if a relevant, reachable threat is found.
The Code
Here is a sanity-audit.ts script you can drop into your pipeline. It uses zod for validation because I do not trust external data, even from npm.
#!/usr/bin/env ts-node
import { readFileSync } from 'node:fs';
import { z } from 'zod';
// 1. Define the Schema for NPM Audit Reports (simplified)
const VulnerabilitySchema = z.object({
name: z.string(),
severity: z.enum(['info', 'low', 'moderate', 'high', 'critical']),
via: z.array(z.union([z.string(), z.object({ title: z.string(), url: z.string() })])),
isDirect: z.boolean(),
effects: z.array(z.string()),
range: z.string(),
nodes: z.array(z.string()),
fixAvailable: z.union([z.boolean(), z.object({ name: z.string(), version: z.string() })])
});
const AuditReportSchema = z.object({
vulnerabilities: z.record(VulnerabilitySchema),
metadata: z.object({
vulnerabilities: z.record(z.number())
})
});
type Vulnerability = z.infer<typeof VulnerabilitySchema>;
// 2. Configuration: What do we actually care about?
const CONFIG = {
// Only fail on these severities
failOn: ['high', 'critical'],
// Ignore vulnerabilities in commonly noisy packages
ignorePackages: [
'loader-utils', // Often flags prototype pollution in build tools
'terser', // Minification issues rarely affect runtime security
],
// Specific advisory titles to ignore after manual review
ignoreAdvisories: [
'Regular Expression Denial of Service', // Context-dependent
]
};
function runAudit() {
try {
// Read stdin
const input = readFileSync(0, 'utf-8');
const report = AuditReportSchema.parse(JSON.parse(input));
const issues = Object.entries(report.vulnerabilities);
const criticalIssues: Array<{ name: string; issue: Vulnerability }> = [];
console.log(`Analyzing ${issues.length} reported vulnerabilities...`);
for (const [name, vuln] of issues) {
// Filter 1: Severity Check
if (!CONFIG.failOn.includes(vuln.severity)) {
continue;
}
// Filter 2: Ignore List
if (CONFIG.ignorePackages.includes(name)) {
console.log(` Ignoring known noisy package: ${name} (${vuln.severity})`);
continue;
}
// Filter 3: Advisory Title Check
const isIgnorableAdvisory = vuln.via.some(cause => {
if (typeof cause === 'string') return false;
return CONFIG.ignoreAdvisories.some(ignore => cause.title.includes(ignore));
});
if (isIgnorableAdvisory) {
console.log(` Ignoring advisory by title for: ${name}`);
continue;
}
// If we are here, it's a real problem
criticalIssues.push({ name, issue: vuln });
}
if (criticalIssues.length > 0) {
console.error(`\nFAILURE: Found ${criticalIssues.length} RELEVANT vulnerabilities:`);
criticalIssues.forEach(({ name, issue }) => {
console.error(` - [${issue.severity.toUpperCase()}] ${name}: ${issue.range}`);
});
process.exit(1);
}
console.log('\nSecurity Audit Passed (Noise filtered).');
process.exit(0);
} catch (error) {
console.error('Failed to parse audit input:', error);
process.exit(1);
}
}
runAudit();Integrating into CI
Now, instead of letting npm audit crash your build blindly, you pipe it into your sanity layer. Update your package.json scripts:
{
"scripts": {
"audit:smart": "npm audit --json | ts-node scripts/sanity-audit.ts"
}
}In your GitHub Actions workflow:
- name: Security Check
run: npm run audit:smart
continue-on-error: falseConclusion
Security is not about having zero reported issues. It is about managing risk. If your tools cry wolf so often that you stop listening, you are infinitely less secure than someone who performs manual, thoughtful reviews quarterly.
The goal is not to ignore security. The goal is to ignore theater. Until our tooling becomes smart enough to understand the difference between a reachable exploit and a regex bug in a dev-dependency, it is up to us to build the filters.
Turn off the noise. Turn on your brain. And please, stop waking me up at 8:00 AM for a ReDoS in a color parser.