Taming SAST False Positives, the Adoption Killer
There is a predictable arc to a badly-run SAST deployment. The tool is switched on at full sensitivity, it returns thousands of findings, developers dutifully investigate the first hundred, discover most are noise, and quietly learn to ignore the rest. Within a month the scanner is producing more findings than ever and influencing nothing. The problem was never the number of bugs; it was the ratio of real to imaginary, and how fast that ratio taught everyone to look away.
A false positive is a finding the tool reports as vulnerable that is not, in fact, exploitable — a tainted path that is actually sanitised, an unreachable branch, a test fixture flagged as production code. They are inherent to static analysis, which over-approximates on purpose to avoid missing real bugs. The goal is never zero; it is a signal-to-noise ratio developers will keep trusting. Precision — the fraction of findings that are real — is the metric that decides whether your program lives.
Where the noise comes from
- Unrecognised sanitizers. Your team validates input through a custom helper the tool has never heard of, so every safe call downstream looks vulnerable.
- Framework blindness. The analyser can't follow data through an ORM, a dependency-injection container, or a templating engine, so it assumes the worst.
- Unreachable paths. A flagged flow is real in the abstract but gated behind a condition that can never be true in practice.
- Context-free rules. A generic ruleset that doesn't know your architecture treats internal, trusted data as if it were attacker-controlled.
Tuning for precision
The fix is deliberate configuration, not resignation. Start by teaching the engine your code: declare your validation helpers as sanitizers and your internal wrappers as safe, and the largest clusters of noise collapse at once. Curate the ruleset to the languages and frameworks you actually run rather than enabling everything. Where the tool supports it, turn on reachability so unexploitable paths drop in severity instead of blocking a merge.
# After declaring escape() as a sanitizer:
0 findings — the tool now knows the value is safe at the sink
Baseline the past, gate the present
A new deployment inherits years of accumulated findings, and blocking on that backlog guarantees revolt. Baseline the existing state — acknowledge it without blocking — and enforce only on newly introduced findings. Developers are then accountable for the code they just wrote, on a small, reviewable diff, which is exactly when a finding is cheapest to understand and fix. Chip at the backlog separately, on its own schedule.
Make suppression a first-class, reviewed act
Some findings really are false, and dismissing them must be easy but never silent. A suppression should require a reason, live in code review, and be visible — so a rushed “false positive” dismissal of a genuine bug gets a second pair of eyes. Track your suppression rate: if it climbs, either a rule needs tuning or your team is waving off real risk. Both are worth knowing.
Precision is what earns a scanner the right to be believed. A tool developers trust changes behaviour; a tool they have learned to mute is telemetry no one reads. Tune first, then discover what your code is really doing — the findings you can act on are the only ones that were ever worth having.
- Sample a handful of recent findings and work out roughly what fraction are real — a precision developers will not trust is the number that kills the program.
- Check whether new findings are gated on the diff while the legacy backlog is baselined, or whether everyone is blocked on years of accumulated noise.
A quick precision sample tells you the trend; tuning rules and baselining an estate back to a trusted signal is what our assessment runs for you.