·9 min read

Cutting Static-Analysis False Positives Without Missing Real Bugs

The false-positive rate is the single biggest reason security scanners get switched off. But naively suppressing findings hides real bugs too. Here are the techniques that lower noise without lowering coverage.

static analysisfalse positivesSAST

Every team that has rolled out a static analyzer knows the failure mode: the tool fires hundreds of findings, a few are real, most aren't, and within a sprint everyone has learned to bulk-dismiss the lot. The scanner is technically still running. It's protecting nothing.

The false-positive rate — the share of findings that aren't real problems — is the metric that governs whether a scanner gets used or quietly abandoned. But the naive fix (suppress more aggressively) trades false positives for false negatives: hide the noise and you hide real bugs with it. The actual engineering problem is lowering noise without lowering coverage. Here's how.

1. Path-aware severity

The same pattern means different things in different places. A hardcoded credential in `src/` is an error; the identical string in `fixtures/` or `*.test.ts` is almost always a test value. A `Math.random()` in production code is a concern; in a test it's usually fine.

Path-aware severity downgrades findings in test, fixture, story, and example paths rather than firing them at full severity. The finding still appears — you haven't blinded the tool — but it doesn't block the build or scream for attention where it almost certainly doesn't matter.

2. Library-aware safe harbours

A huge fraction of false positives come from not recognising that the code already did the safe thing. A money value in a float is a bug — unless the file imports decimal.js or big.js, in which case the arithmetic is exact. A raw SQL string is injection-shaped — unless it's a parameterised query through a known ORM. An unbounded retry loop is a problem — unless it's wrapped by p-retry.

// Flagged: money in a float
const total = parseFloat(req.body.amount);   // money-float: error

// Safe-harboured: the file uses a decimal library
import Decimal from "decimal.js";
const total = new Decimal(req.body.amount);   // no finding

3. Explicit, auditable suppression

Sometimes the tool is right that a pattern is present and the developer is right that it's fine. The answer isn't to weaken the rule globally; it's a local, explicit, reviewable suppression — a `// gatetest-ok` style marker on the line, which shows up in code review so a teammate can see the override and the reason.

This keeps the decision where it belongs (with the author, in the diff) instead of in a global config file nobody reads, and it leaves an audit trail.

4. Confidence scoring and gating only on high confidence

Not every true finding is equally certain. A detector can often tell the difference between 'this is definitely a leaked AWS key' (high confidence) and 'this string has the entropy of a secret but might be a hash' (lower confidence). If you gate — block the build — only on high-confidence findings and let the rest surface as warnings, you get the enforcement benefit without the noise cost.

5. Learn from dismissals

The strongest signal about whether a rule is noisy is how often humans dismiss it. If a particular rule is suppressed by customers 90% of the time, that's data: the rule is probably miscalibrated. A feedback loop that watches dismissal rates and recommends severity downgrades turns the crowd's judgement into calibration, so the scanner gets quieter over time exactly where it's wrong — and stays loud where it's right.

The principle underneath

Every one of these techniques shares a goal: be quiet where you're probably wrong, loud where you're probably right, and never silently drop a finding — downgrade it visibly instead. That's how you earn the one thing a scanner can't function without, which is the developer's trust that a finding is worth looking at.

Frequently asked questions

What is a good false-positive rate for a SAST tool?

Lower is better, but the more important property is that the tool gates only on high-confidence findings and surfaces the rest as warnings. A scanner that blocks builds on noisy findings gets switched off regardless of its raw rate.

Doesn't suppressing findings hide real bugs?

Naive global suppression does. The techniques that don't — path-aware severity, library-aware safe harbours, explicit per-line suppression, and confidence gating — lower noise without dropping findings silently. A downgraded finding still appears; it just doesn't block.

Put a gate between your AI and your main branch

110 modules. Pay per scan, no subscription. AI auto-fix PR on the Scan + Fix tier.

Run a scan — from $29

Keep reading