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)
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 (23)
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?
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.
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.
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.
Leaving the uncertain ones out of the bulk action is the safer default.
Yeah, and it doesn't have to be complicated either. Keep the bulk action for findings that clearly meet the same criteria, and make exceptions visible instead of silently including them. I'd rather have a reviewer investigate a few extra alerts than explain later why one real secret slipped through.
Making the exceptions visible is the useful part there. A reviewer can check a short visible list, but a silent inclusion leaves nothing to check.
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
I agree with keeping deterministic verification in the loop, especially when the model proposes a patch or reproduction test. I'd be careful about making a passing test the only route to closure, though. Some findings are resolved through configuration changes, architectural controls, or removing the vulnerable code entirely. The common requirement should be verifiable evidence tied to the exact commit, not necessarily one specific kind of test. The model can help get there, but it shouldn't own the final decision.
Two separate problems in your last replies, and they need different fixes.
A stable identity keyed on normalized location still moves. Split a file, pull a function into its own module, extract a control flow: the code relocates and the finding is the same weakness. Rule plus location turns a refactor into a new finding and orphans the old dismissal. Rule plus the artifact the check actually produced (the matched AST shape, the taint path, the fixture that fired) survives the move, and it is the thing a second reviewer can rerun. That is what makes evidence at closure checkable instead of merely mandatory. "Validated upstream" is prose; a check, its artifact, and the commit are a record.
You are also right that a passing test cannot be the only exit, because a config change and deleted code have no test. The common requirement is an artifact that ties the closure to that commit, whatever produced it.
Whether a claimed fix actually happened is the work I do. If you want the longer version aimed at Cyclopt's pipeline instead of the general argument, it is $25, agree first, no link until you say yes. The thread already has the substance; the paid part is the working design.
The fmt example is the whole problem in one snippet. The summary was right about a variable the rule never fired on. One check I'd add: make the AI explanation name the exact sink and trace hop it's reasoning about, and reject any "likely false positive" that doesn't reference the same line the rule flagged. That catches the wrong-variable case mechanically instead of hoping a reviewer notices.
On false dismissals, the cheapest signal I've found is re-detections. If the same rule fires in the same function after a refactor and the earlier finding was closed on an AI note, that's a dismissal you go back and grade. It's not a full accuracy rate, but it's free and it points straight at the bad closes.
Re-detections are the right free signal, and they grade the code more often than they grade the close. The same rule firing in the same function after a refactor can mean the earlier close was wrong, or that the sink moved out from under a check that was fine. Without the check that closed it and the commit it ran at, you cannot tell those apart, so the re-detection points at a bad close without convicting it. The commit is what turns go-back-and-grade into a query: diff from there, rerun the recorded check, and it either still passes (the close was wrong) or fails on code that changed (the close was right and the code moved).
Whether the signal is countable at all depends on how the scanner keys a finding. If it keys on rule plus function instead of line number, a rename keeps the key and you can count it. If it keys on line, every refactor mints new findings and the old ones are gone, so the free signal only ever counts the closes that did not matter.
Does yours survive a function rename?
That's a really important distinction. A re-detection is a signal, not a verdict. Without knowing what check ran and what commit it ran against, you can't tell whether the original dismissal was wrong or the code simply changed later. On the function rename question, I'd favor a stable finding identity based on the rule and normalized code location or data-flow context, rather than line numbers alone. Refactors shouldn't erase the history of a finding.
I like the re-detection idea. It's a practical signal teams can start tracking without building a whole accuracy framework first. And requiring the explanation to reference the exact sink and line makes sense too. I'd still keep re-detections as something to investigate rather than automatically count as false dismissals, since the code may have changed in between.
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.
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.
hordii_bohdan has the cost right and the substitution wrong. The triage loop does not replace an AST check. It replaces a person reading the same finding, and the pipeline pays for that reading anyway, only later and without a record.
My point is the exit. Whatever proposed the close, model or person, the close names the check that ran, the artifact it ran against, and the commit. madhabcoder's list is the one I would enforce: rule ID, file and line, link to the fix. gautamtalksdev's test is the cheapest of all: reject a close that argues about a symbol the scanner never fired on.
None of that is inference. It is three required fields at the moment a finding becomes invisible.
I like that framing: the exit is where the accountability needs to live. It doesn't really matter whether the AI or a human proposed the dismissal if the system can't show what actually justified it. And that symbol check is a great practical safeguard. If the explanation doesn't address what triggered the finding, it shouldn't even be eligible for closure. Simple rule, but it could prevent a lot of bad dismissals.
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.
Exactly. The link is what makes the decision useful six months later. "Validated upstream" sounds reasonable until someone has to find where that validation actually happens. I'd make the evidence mandatory at closure, not an optional field people fill in when they remember. The AI can write the explanation, but the record needs something another reviewer can independently verify.
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?
That's a good check. The wrong-variable case is exactly the kind of mistake we should be able to catch without relying on the reviewer to spot it. If the explanation can't point back to the actual sink and trace that triggered the rule, it shouldn't be allowed to recommend closure. It won't prove the finding is valid, but at least it stops the AI from confidently answering a different question.
The fmt-versus-tenant example is the sharpest failure mode here — the summary was correct about the wrong variable and got accepted precisely because it read well. That is why "AI as annotation, never replacement" is the load-bearing rule; a summary that can't point at the rule ID and trace is an opinion wearing a verdict's clothes. Do you track false dismissals by sampling closed findings, or is that the part teams skip because the throughput number looks too good to question?