Security7 min read

False positives are a culture problem before they are a model problem

The easiest way to drive a false positive rate to zero is to turn the scanner off. Most teams do not turn the scanner off. They start not reading it instead. What we keep finding at customer sites is not a model accuracy problem but an operating problem.

By FloatFactory Security Engineering

FlawDetector engine · research

3.2% is enough to break a team

In a benchmark, a 3.2% false positive rate is a good number. How many items that becomes in a real queue is decided by codebase size. Finding counts scale close to linearly with lines of code, and triage time scales with finding counts.

Weekly triage load implied by a 3.2% false positive rate
CodebaseFindings per weekFalse positives per weekTriage time (12 min each)
50k lines~120448 minutes
400k lines~900295.8 hours
2M lines~4,30013827.6 hours

12 minutes is our observed median per finding, including reading the code, attempting reproduction and recording the verdict.

27.6 hours a week is 0.7 of a security engineer, and the deliverable of that work is the conclusion that nothing happened. No organisation staffs that role for long. So the outcome is always the same shape: the queue backs up, a backed-up queue stops being read, and real findings get buried alongside the noise.

The cost of a false positive is trust, not time

Most estimates of false positive cost count triage hours. The expensive part happens afterwards. A developer who hits three false positives in a row does not open the fourth alert. That lesson does not stay personal — it becomes the team default. Once "the security bot is complaining again" is an established joke, the effective detection rate of that pipeline is zero regardless of what the benchmark says.

Alert fatigue is not people getting lazy. It is what happens when the signal-to-noise ratio crosses the point where ignoring becomes the rational strategy.

Four different things people call a false positive

When we analysed customer triage records, fewer than half of the findings closed as false positives were cases where the engine was actually wrong. The rest were cases where the engine was right and the organisation decided not to act. Closing both with the same button is where the trouble starts.

What closed findings actually were
ActuallySymptomCorrect response
A true false positiveThe flaw is not present in the codeSubmit engine feedback and adjust the rule. Accuracy discussions belong only here
UnreachableThe flaw is real but no external input reaches itRecord the reachability argument and suppress with an expiry
Accepted riskKnown, and deliberately carriedAn exception with an owner, a compensating control and a review date
Prioritisation callReal, but not the thing to fix nowMove to the backlog rather than suppress — it should reappear next scan

The false positives a team creates for itself

How a tool is rolled out often matters more than the tool. Three patterns recur.

  • A full scan with no policy — turning every rule on across the whole codebase on day one dumps thousands of legacy items in with the new ones. From a developer's seat, that entire queue is noise.
  • Reports with no gate — a report that blocks nothing gets read by nobody. Block every severity from the start and the team turns the gate off. A narrow gate on critical findings in new code had by far the best survival rate.
  • An ownerless queue — if findings are not auto-assigned, the queue becomes a commons. Wire it to your code-ownership file so the responsible team is set the moment a finding is created.

Five practices that work

  1. 01

    Set a triage SLA

    Critical findings get a first verdict within 24 hours, high within three business days. A verdict, not a fix. Unjudged findings piling up is more dangerous than unfixed findings piling up.

  2. 02

    Force expiry dates on suppressions

    No permanent suppressions. Cap them at 180 days; when one expires the finding reopens on the next scan. Context changes, and last spring's "unreachable" can be reachable today.

  3. 03

    Agree on what reachable means

    Teams disagree on what counts as attacker-controlled input. Internal admin APIs? Batch job arguments? Without writing this down first, every triage turns into the same argument.

  4. 04

    Give every finding an owner

    Assign the responsible team at creation time from code ownership metadata. Reassignment is fine; unowned is not.

  5. 05

    Run a quarterly false positive retro

    Take only the cases classified as true false positives and spend 30 minutes sorting them into: fixable by rule tuning, needs framework awareness, or our own code convention should change.

Suppressions belong in the repository

If a suppression exists only as a click in a dashboard, in six months nobody knows who clicked it or why. Keep suppressions as a reviewed file in the repository and enforce expiry in CI.

.flawdetector/suppressions.ymlyaml
- finding: FD-2026-05-0417
  rule: CWE-22/path-traversal
  path: internal/tools/migrate.go
  class: unreachable          # false-positive | unreachable | accepted-risk
  reason: >
    CLI-only path. The input path is produced by the deploy pipeline
    and is never exposed on the HTTP surface. See threat model.
  evidence: docs/threat-model.md#migrate-cli
  owner: platform-team
  expires: 2026-11-30         # reopens automatically on the next scan

- finding: FD-2026-05-0902
  rule: CWE-327/weak-hash
  path: legacy/auth/session.rb
  class: accepted-risk
  reason: "Legacy session compatibility; removed by the 2026-09 migration."
  compensating-control: "15-minute session TTL plus IP binding"
  evidence: JIRA-SEC-2841
  owner: identity-team
  expires: 2026-09-30
Owner, evidence and expiry are required fields

Then lint the file itself in CI. Expired suppressions, broken evidence links and suppressions owned by a team that no longer exists all fail the pipeline.

bashbash
# expiry and orphan check (run it on a weekly schedule)
flawdetector suppressions lint \
  --max-age 180d \
  --require-evidence \
  --require-owner \
  --fail-on expired,orphaned

# review what is currently hidden
flawdetector scan --include-suppressed --format table

What to measure instead

False positive rate is a metric for evaluating a tool, not for evaluating a team. To see whether a security programme is healthy, look at these.

Security queue operating metrics
MetricDefinitionTarget
Triage lead time (P50)Finding created → first verdictUnder 24 hours
Suppression expiry complianceShare of suppressions past their expiry date0%
Reopen rateUnsuppressed findings that are re-confirmed vulnerableUnder 5%
Critical seal lead timeDetection → sealed verdictUnder 72 hours

With those four healthy, a pipeline survives a 3% or a 5% false positive rate. With those four broken, a 1% tool changes nothing: the queue still backs up and the alerts are still ignored.

In summary

Raising accuracy is the engine's job and we keep doing it. But in the field the variable that decided success was almost always operational. Do suppressions expire? Do findings have owners? How long until the first verdict? Teams that settled those three managed their queue with whatever tool they had; teams that did not were buried under every tool they tried.

The decision to make before you adopt a scanner is not a threshold. It is who closes a finding, by when, and on what evidence.

Frequently asked questions

Does a more accurate tool solve the triage problem?
It reduces the load but does not solve it. Most triage time goes to reachability judgements and risk-acceptance decisions, not to true false positives. Those are calls the organisation has to define regardless of tool accuracy.
Doesn't forcing expiry dates on suppressions add overhead?
In the short term, yes. But suppressions without expiry drift into a list of findings nobody can explain, and that cost comes due during an audit or an incident. With a 180-day cap and automatic reopening, the practical renewal effort is roughly once a quarter.
Which severity should the merge gate start at?
Start by blocking only critical findings in new code. Gating existing debt at the same time usually leads to the gate being switched off. Keep debt in a separate backlog and burn it down at its own pace.
Should unreachable findings stay in the report?
Yes. Unreachability depends on code structure and a single refactor can invalidate it. Recording the argument alongside an expiry date gives you the basis to revisit it once the structure changes.

Keep reading

All articles

See the loop run on your own repository

Connect a repo and get a full scan, a merge-ready patch, and a verdict log for every critical finding. No card required.