DEV Community

Dimitris Kyrkos
Dimitris Kyrkos

Posted on

An AI summary is not a fix. Stop letting it close your security findings.

Static analysis and LLMs are a good pairing. I'm not here to argue otherwise. But a lot of teams are wiring them together in a way that quietly turns the scanner's evidence into the AI's opinion, and then treating the opinion as the record.

That's the part worth talking about.

The demo vs. the pipeline

In the demo, it looks great. The scanner dumps 400 alerts on a pull request. An assistant reads them, groups them into twelve clusters, writes a plain-English paragraph for each, and marks the ones it thinks are noise. The reviewer sees twelve tidy cards instead of a wall of rule IDs. Everyone relaxes.

In the pipeline, six months later, nobody can tell you why a SQL injection finding in orders/export.py was closed. The ticket says "Resolved: input is validated upstream." There's no link to the upstream validation. The original rule ID, file, line, and data-flow trace are gone, because the summary replaced them.

The scanner and the assistant have different jobs. The scanner says: this rule was violated at this location, and here is the path that got there. The assistant can say: here's what that means, here are three other alerts that are probably the same root cause, and here's where I'd look first. Problems start when the second job silently absorbs the first.

Failure mode 1: the explanation becomes the verdict

Illustrative example. A taint-tracking rule flags this:

def export(request):
    fmt = request.GET.get("format", "csv")
    query = f"SELECT * FROM orders WHERE tenant = '{request.user.tenant}' ORDER BY {fmt}"
    return run(query)
Enter fullscreen mode Exit fullscreen mode

The assistant reads it and writes: "The tenant value comes from the authenticated user object and is not user-controlled. Likely false positive." That sentence is correct about tenant. It says nothing about fmt, which is the actual sink the rule fired on. The summary was plausible, focused on the wrong variable, and got accepted because it read well.

An explanation is a hypothesis about a finding. It is not evidence that the finding is resolved.

Failure mode 2: grouping hides the odd one out

Clustering is genuinely useful. Forty "hardcoded credential" alerts that are all test fixtures should collapse into one decision.

But clusters get dismissed as a unit. If 39 of those 40 are fixtures and one is a real key in deploy/config.py, a "dismiss cluster" button throws it out with the rest. The grouping logic needs to show you what makes each member similar, and the dismissal needs to be recorded per finding, not per cluster.

Failure mode 3: the evidence doesn't survive the summary

If your triage flow stores "AI says false positive" and drops the rule ID, location, trace, and the code snapshot it was judged against, you can't audit it later. You can't rerun the check against the fixed code. You can't notice that the same pattern came back in a refactor.

Keep the original finding as the source of truth. Attach the AI explanation next to it, labeled as an explanation. Close the finding only when a rescan of the changed code no longer reports it, or when a human records a reason that points at something concrete.

Failure mode 4: you're measuring throughput, not accuracy

"The assistant processed 12,000 alerts this quarter" is a volume metric. It tells you nothing about whether the right ones were dismissed.

The number that matters is false dismissals: findings the AI (or a human following the AI) closed that were real. You only learn that rate by sampling closed findings and re-reviewing them, or by tracking findings that were dismissed and later reappeared as incidents or re-detections. If you aren't measuring it, you don't know whether your triage got better or just got faster.

So what does a sane setup look like?

  • Scanner output is immutable. Rule, location, trace, commit.
  • AI output is an annotation on that record, never a replacement for it.
  • "Resolved" requires either a clean rescan or a human reason with a pointer to code.
  • Cluster dismissals expand into per-finding records.
  • You periodically sample dismissed findings and track how many were wrong.

None of this is exotic. It's the same discipline you'd apply to a junior reviewer: helpful, fast, often right, and not the one who gets to sign off alone.

AI really does make security signals easier to understand. The evidence still has to survive the summary.

How are you handling AI-assisted triage on your team? Do you track false dismissals at all, or is "alerts processed" the only number anyone looks at?

Top comments (10)

Collapse
 
robertpaschal profile image
Odinaka Robert Nnamani •

The cluster example is the sharpest one here: 39 fixtures hiding one real key in deploy/config.py. What would make a dismiss button show that odd one out before it lets the whole cluster go?

Collapse
 
cyclopt_dimitrisk profile image
Dimitris Kyrkos •

I’d make the button show the outliers before allowing a bulk dismissal. Things like different file paths, production configs, or secrets that don’t match the rest of the cluster should get flagged for individual review. And honestly, if even one finding looks different enough, it should be excluded from the bulk action by default. The goal is to make dismissing 39 findings easy without making the 40th disappear with them.

Collapse
 
robertpaschal profile image
Odinaka Robert Nnamani •

That default exclusion is the key part for me. A bulk button should not need a second look to catch the one file that does not belong.

Thread Thread
 
cyclopt_dimitrisk profile image
Dimitris Kyrkos •

Exactly. The default should be conservative, not make the reviewer responsible for spotting every outlier in a long list. If the system isn't confident a finding belongs in that cluster, leave it out of the bulk action and let someone review it separately. A few extra clicks are a much better tradeoff than silently closing a real vulnerability.

Thread Thread
 
robertpaschal profile image
Odinaka Robert Nnamani •

Leaving the uncertain ones out of the bulk action is the safer default.

Collapse
 
guardilands profile image
Guard •

Alert volume is easy to instrument. False dismissals aren't, unless every close carries the check that closed it.

Three fields on the dismissal record: which check ran, what artifact it ran against, which commit. Then the false-dismissal rate is a query instead of a sampling project. Rerun the recorded checks against current artifacts, count the ones that come back red. A close with only an explanation attached is not a pass, it's an unverified close. Count it separately or it sits in the denominator pretending to be a decision.

Your cluster case has the same shape. Similarity ranks the 39+1, it does not check them. Two findings looking alike does not mean both are fixtures, and the one live key is not distinguishable by how similar it looks. Only a check per member separates them, so what the record has to hold is the check, not the neighbor.

Adjacent receipt from this week. Apple published a block on eight compromised eMudhra intermediate CAs. Two write-ups then disagreed about whether /usr/bin/curl on macOS still trusted them, both with sources. Reading the block notice settles nothing, because macOS carries two trust stores: the keychain, and a flat bundle shipped inside the OS image that curl and other file-based tools read. "Revoked" was the claim. Which store the process consults was the state, and only opening that file tells you which one you have.

That is failure mode 3 with an extra layer: name the artifact the runtime actually consults, not the one the summary cites.

Collapse
 
cyclopt_dimitrisk profile image
Dimitris Kyrkos •

The distinction between a check and an explanation is a good one. I especially like the idea of treating unverified closes as a separate category, because otherwise the metrics can look healthy while the actual risk stays hidden. And the artifact point matters too. Even if a check passed, you need to know exactly what it ran against and which commit it covered. Otherwise, the record gives you confidence without giving you a way to reproduce the decision.

Collapse
 
madhabcoder profile image
Nilamadhab Senapati •

This one hit home. 'Resolved: input is validated upstream' with no link isn't a record, it's a vibe. I'd let the AI group and explain findings all day, but closing one should still need the original rule ID, the file and line, and a link to the actual fix or validation. Otherwise six months later you're doing archaeology on your own repo.

Collapse
 
hordii_bohdan profile image
Bohdan Hordii •

Automating vulnerability triage with an LLM usually boils down to teams hiding an uncalibrated SAST noise floor behind non-deterministic text generation. You replace an instantaneous, zero-cost deterministic AST check with an expensive inference loop—spiking pipeline latency by 1.2s to 3.5s per finding, hogging 16–24GB of VRAM on inference nodes or burning $0.02 to $0.05 per API call—all to get an eloquent hallucination that flags a real tainted-data flow or memory corruption path as a "harmless false positive."

If you're using models in security triage, keep them advisory and strictly gated behind deterministic verification: have the model propose a minimal reproduction test or patch, spin it up in an isolated cgroup or throwaway container, and only alter ticket state if the test provably passes against the binary. When teams bypass that feedback loop and wire an LLM straight to the close button, what trade-off are they actually accepting—shaving minutes off a sprint review at the cost of your audit trail and real attack surface?

Bohdan Hordii // bohdanhordii.dev

Collapse
 
gautamtalksdev profile image
Gautam K •

The fmt example suggests a cheap mechanical check: make every proposed close name the variable and line from the scanner's own trace, and reject it when the explanation argues about something that is not on that trace. Here the summary reasoned about tenant while the rule fired on fmt in the ORDER BY. It will not catch every mistaken verdict, but it catches the case where the reasoning is about a different variable than the finding. Does your pipeline keep the trace in a form an explanation could be checked against like that?