Over 92% of teams use AI coding assistants, and AI-written code is now a top AppSec blindspot: new dependencies, new sink flows, embedded secrets.
More than nine in ten engineering teams now ship code an assistant helped write, and the assistant does not get tired, does not slow down, and does not stop to ask whether the pattern it just produced is safe. The result is not a new class of vulnerability so much as a new volume of an old one: the same insecure shape, an unparameterized query, a hardcoded token, a missing authorization check, stamped out across dozens of files in a single afternoon. The attack surface did not get more exotic. It got bigger and more repetitive at the same time.
That changes the economics of review, not the nature of the bug. A human writes one vulnerable handler and a reviewer might catch it. An assistant writes fourteen handlers from the same prompt, all sharing the same flaw, faster than any reviewer reads. The honest answer is not to ban the assistant. It is to put the assistant's output through exactly the proof bar you already apply to a human's, and to make that bar scale with the volume.
AI-written risk does not arrive through a single channel. It arrives through three at the same time, and a tool that only watches one of them misses most of it. The first door is dependencies: the assistant pulls in an open-source package to satisfy an import without anyone deciding to trust it. The second is taint flow: generated logic wires user input into a sink that no human chose to connect. The third is secrets: a credential gets pasted, or invented, straight into the source.
Each door needs a different oracle. A dependency is a question about provenance and reachability. A taint flow is a question about source-to-sink data movement. A secret is a question about what literally sits in the file and the git history behind it. apPosture watches all three and, critically, refuses to treat any of them as a finding on appearance alone.
The signature of AI-generated risk is repetition. One prompt produces many near-identical sink sites, and a pattern matcher reacts to each of them as if it were a separate discovery. That is how a single bad habit becomes a wall of forty rows that a reviewer stops reading at row five.
The difference is not cosmetic. A pattern matcher tells you a string looks like a SQL concatenation. Inter-procedural taint tracking tells you that a value from an HTTP parameter actually flows, across function and file boundaries, into a query that hits the database. The first is a guess about syntax. The second is a fact about data movement, and only the second earns a place at the top of a queue.
The distinction is worth making concrete, because it is the whole game. Two engines can look at the same generated function and reach opposite conclusions about whether it is worth a human's attention.
| Dimension | Pattern match (noisy) | Proven sink (apPosture) |
|---|---|---|
| Signal | Code resembles a known-bad shape | Tainted input reaches a real sink across files |
| Evidence | Regex or AST shape match | Source-to-sink taint path with a reachable boundary |
| Repeated codegen | N lookalikes, N separate rows | One finding per file:line, siblings correlated by class |
| Reviewer cost | Triage every row by hand | Read the proof, fix the root pattern once |
| Failure mode | False positives drown the real one | Shorter list, every item survives audit |
Proving the sink is what lets the volume problem solve itself. When the engine knows a flow is real, it also knows which of the forty lookalikes share the same weakness class and the same root cause, and it can say so without pretending each one is a fresh incident.
Dedup is where the repetition stops being overwhelming. apPosture gives every code weakness a stable identity built from its weakness class and its exact source location: vuln_class | file:line. The class comes from the CWE when one is known and falls back to a normalized name; the location is the file and line where the sink lives.
This is the right granularity for generated code. You do not want forty rows for one prompt, and you also do not want one row that silently hides thirty-nine distinct fixes. Keying on file:line means a repeated SQL injection in four files surfaces as four addressable units, while the same finding re-reported by a second scan or a second tool merges into the record you already have. The signal stays high without losing a single place a developer has to edit.
None of this treats AI-written code as a special category, and that is the point. The engine does not ask who wrote the line. Inter-procedural taint tracking follows the flow, SCA checks every newly pulled dependency for reachability and malicious-package signals, and secrets scanning reads both the current head and the git history behind it. A generated handler that passes user input into a query is a real flow, not a style nit, and it clears the same bar a human's handler would.
The loop closes in the pipeline. The assistant proposes, the engine proves, and the gate blocks net-new risk before it reaches a release. Because findings carry proof rather than suspicion, the gate can fail closed without burying the team in noise: the list it hands back is short, deduped to one unit per real fix, and every item on it survives the same audit a human reviewer would have demanded. AI made the volume of code go up. It does not get to make the standard of evidence go down.