A static auditor for AI systems. What it detects, how well, on what — and what it deliberately does not claim.
Generated from labeled data · drift fails the buildAn AI system fails in ways ordinary code review does not catch. Probanza reads a codebase and finds those places before they ship.
It is a static auditor: it finds the exposure, it does not stop an attack. Every count below is what it found in real, shipping open-source products — hand-labeled, one finding at a time.
If your assistant summarizes a web page, a support ticket, or an uploaded document, whoever wrote that text can hide instructions inside it. Probanza finds the places where outside text reaches the model's own instructions with nothing separating the two — the boundary that stops a retrieved document from talking to your agent as if it were you.
Found across 6 real products. Measured by PROMPT_INJECTION_UNTRUSTED_BLENDING · PROMPT_INJECTION_SECRECY_POLICY · PROMPT_INJECTION_CROSSFILE_BLENDING — precision 1.000.
A model's output flows onward into a database write, a workflow, another API call — with nothing verifying it is even the right shape. Probanza finds those unguarded hand-offs: the model calls and prompts whose result no schema, validator, or parser ever inspects.
Found across 11 real products. Measured by AI_MODEL_CALL_WITHOUT_VALIDATOR · PROMPT_MISSING_STRUCTURED_OUTPUT · AI_PROMPT_WITHOUT_VALIDATOR — precision 0.991.
Pricing thresholds, eligibility rules and scoring formulas written into a prompt are decided by a language model — differently each time, with no audit trail, and invisible to the tests that cover your code. Probanza finds the rules that drifted out of your codebase and into English.
Found across 4 real products. Measured by PROMPT_CONTAINS_DETERMINISTIC_FORMULA — precision 1.000.
Hardcoded credentials, unsafe evaluation, shell execution — the familiar risks, with the familiar problem: a scanner that shouts about every one of them trains your team to ignore all of them. Probanza detects each, then classifies what kind it is, so a key your vendor publishes on purpose never arrives wearing the same red as a leaked one.
Found across 3 real products. Measured by HARDCODED_CREDENTIALS · UNSAFE_EVAL_USAGE · SHELL_EXECUTION — precision 1.000. Of the 17 credentials it found across these products, 0 are security incidents.
The tables below use the vocabulary of detection measurement. It decodes to four ideas:
A tool can look perfect by flagging almost nothing — high precision, terrible recall — or by flagging everything, which is perfect recall and unusable noise. Both numbers are published here for every rule, which is the only way either one means anything.
Every number is computed from hand-labeled CSVs at build time. None is transcribed by a person, and the build fails if this page drifts from the labels. That is not fastidiousness: prose did drift here once, and it hid a real false positive.
| Rule | TP | FP | FN | Labeled set | Precision | Recall | n | Record |
|---|---|---|---|---|---|---|---|---|
| ACCESS_CONTROL_UNSCOPED_OBJECT_LOOKUP | 19 | 0 | 0 | 1.000 | 1.000 | 19 | 2026-07-13l · bundle | |
| AI_AGENT_SHELL_WITHOUT_APPROVAL | 1 | 5 | 1 | 0.167→1.000 | 0.500 | 7 | 2026-07-14r · bundle | |
| AI_MODEL_CALL_WITHOUT_VALIDATOR | 1200 | 9 | 4 | 0.993 | 0.997 | 1213 | 2026-07-14u | |
| AI_PROMPT_WITHOUT_VALIDATOR | 22 | 2 | 0 | 0.917→0.917 | 1.000 | 24 | 2026-07-13f · bundle | |
| HARDCODED_CREDENTIALS | 17 | 53 | 0 | 0.243→1.000 | 1.000 | 70 | 2026-07-12c | |
| PROMPT_CONTAINS_DETERMINISTIC_FORMULA | 6 | 0 | 0 | 1.000 | 1.000 | 6 | 2026-07-14u | |
| PROMPT_INJECTION_CROSSFILE_BLENDING | 24 | 0 | 1 | 1.000 | 0.960 | 25 | 2026-07-13n · bundle | |
| PROMPT_INJECTION_SECRECY_POLICY | 3 | 0 | 0 | 1.000 | 1.000 | 3 | 2026-07-12j · bundle | |
| PROMPT_INJECTION_UNTRUSTED_BLENDING | 1 | 0 | 0 | 1.000 | 1.000 | 1 | 2026-07-12j · bundle | |
| PROMPT_MISSING_STRUCTURED_OUTPUT | 32 | 2 | 0 | 0.941 | 1.000 | 34 | 2026-07-14u | |
| SHELL_EXECUTION | 1 | 0 | 0 | 1.000 | 1.000 | 1 | 2026-07-12c | |
| SHELL_EXECUTION | 30 | 0 | 0 | 1.000 | 1.000 | 30 | 2026-07-14r · bundle | |
| TEST_GAP_DETECTED | 51 | 0 | 0 | 1.000 | 1.000 | 51 | 2026-07-13f · bundle | |
| TEST_GAP_DETECTED | 75 | 0 | 0 | 1.000 | 1.000 | 75 | 2026-07-14q · bundle | |
| TEST_GAP_DETECTED | 144 | 5 | 0 | 0.966→1.000 | 1.000 | 149 | 2026-07-14v · bundle | |
| TEST_GAP_DETECTED | 800 | 6 | 0 | 0.993→0.999 | 1.000 | 806 | 2026-07-14y · bundle | |
| TEST_GAP_DETECTED | 158 | 26 | 0 | 0.859→1.000 | 1.000 | 184 | 2026-07-14ab · bundle | |
| TEST_GAP_DETECTED | 799 | 49 | 0 | 0.942→1.000 | 1.000 | 848 | 2026-07-14ah · bundle | |
| TEST_GAP_DETECTED | 229 | 27 | 0 | 0.895→0.947 | 1.000 | 256 | 2026-07-16b · bundle | |
| UNSAFE_EVAL_USAGE | 11 | 10 | 0 | 0.524→1.000 | 1.000 | 21 | 2026-07-12c |
true positive false positive false negative bar shows the shape of the labeled set
Two rules carry an arrow. They are rules whose false positives we found in our own tool and fixed. The left number is what the rule actually scored when we labeled it; the right is what it scores after the fix.
We publish both, because a product that only ever shows you its final number is asking you to trust that it looked.
AI_AGENT_SHELL_WITHOUT_APPROVAL — 5 false positives found and eliminated at 2026-07-19b: precision 0.167 → 1.000 over 1 surviving true positives.AI_PROMPT_WITHOUT_VALIDATOR — 2 false positives found and eliminated at 2026-07-17a: precision 0.917 → 0.917 over 22 surviving true positives.HARDCODED_CREDENTIALS — 53 false positives found and eliminated at 2026-07-12c: precision 0.243 → 1.000 over 17 surviving true positives.TEST_GAP_DETECTED — 5 false positives found and eliminated at 2026-07-14x: precision 0.966 → 1.000 over 144 surviving true positives.TEST_GAP_DETECTED — 6 false positives found and eliminated at 2026-07-14aa: precision 0.993 → 0.999 over 800 surviving true positives.TEST_GAP_DETECTED — 26 false positives found and eliminated at 2026-07-14ae: precision 0.859 → 1.000 over 158 surviving true positives.TEST_GAP_DETECTED — 49 false positives found and eliminated at 2026-07-16a: precision 0.942 → 1.000 over 799 surviving true positives.TEST_GAP_DETECTED — 27 false positives found and eliminated at 2026-07-17a: precision 0.895 → 0.947 over 229 surviving true positives.UNSAFE_EVAL_USAGE — 10 false positives found and eliminated at 2026-07-12c: precision 0.524 → 1.000 over 11 surviving true positives.A rule can score beautifully on the code its author was staring at while writing it. The only honest test is code the rule has never seen — so five products (browser-use, private-gpt, open-webui, LibreChat, Flowise) were acquired after the same-file rules were built, and those rules met them cold.
| Rule | Designed on | Benchmark TP | Acquired TP | Acquired FP | What the acquired slice tests |
|---|---|---|---|---|---|
| PROMPT_INJECTION_SECRECY_POLICY | the 15 benchmark products | 3 | 0 | 0 | specificity — silence, verified correct |
| PROMPT_INJECTION_UNTRUSTED_BLENDING | the 15 benchmark products | 1 | 0 | 0 | specificity — silence, verified correct |
| PROMPT_INJECTION_CROSSFILE_BLENDING | all 20 products | 16 | 8 | 0 | nothing — in-sample by construction |
What this proves. Run cold against 5 unfamiliar products and 5,592 files, the same-file rules raised 5 false positives — and a manual source-to-prompt census confirmed the silence was correct: those products defend their prompts properly, delimiting untrusted text and carrying it in its own message role. The rules did not overfit to noise. Pointed at unfamiliar, well-built code, they had the good sense to say nothing.
What this does not prove — and we will not pretend otherwise. They also found nothing new there. Zero true positives in the acquired slice makes this a test of specificity, not of discovery. It shows the rules do not invent findings in code they have never read; it does not show they would find real ones.
The cross-file rule does find real things across unfamiliar products — 8 of its 24 true positives live outside the benchmark, at 1.000 precision, so it is plainly not a two-product special case. But it was designed with all twenty products already in view. Its numbers are in-sample by construction, and it has not yet had a true out-of-sample test. Claiming one would be the cheapest lie in this document.
The experiment that would settle it
Acquire products the cross-file rule has never seen, run it cold, and label every fire by hand — the same way everything else here was measured. Until that runs, the honest answer to "does it generalize?" is: the specificity half is proven; the discovery half is not.
Across every rule, the acquired slice contributes 45 true positives and 5 false positives.
The credential rule detects credentials. Whether a credential is an incident is a second question — and answering only the first is how security tools earn their reputation for noise.
A PostHog project key is a hardcoded credential. The vendor also publishes it on purpose, to be embedded in client code. Calling that CRITICAL is not a detection error; it is a risk-classification error. So the rule keeps detecting every credential, and a taxonomy decides what kind it is.
Across the corpus's 17 detected credentials
0 are security incidents. The rest are true findings that need no action — and they are still reported, with their class, because deleting them would destroy traceability.
What they turned out to be:
Two properties keep the taxonomy honest. A recognized secret format beats context: an AWS key inside a CI workflow is still a leaked AWS key, and a dev-looking location never rescues real secret material. And ignorance never downgrades: a credential the taxonomy does not recognize stays CRITICAL as unclassified, which makes that count a quality metric rather than a hiding place.
A security tool's real price is not its licence. It is the hours an engineer spends triaging what it says — and a tool that shouts about everything teaches its users to hear nothing.
Two arcs did this without weakening detection by a single finding: one removed the rules' false positives, the other stopped calling public-by-design credentials incidents. A team triaging the security rules across these twenty products would have opened 122 alerts. Today they open 42 — 66% fewer, and every true positive is still found.
What this number is not
It is measured against this tool's own earlier behaviour — not against another vendor's scanner. We have never run one, so we do not claim a ratio against one. And the number belongs to this corpus; what generalizes is the mechanism, not the percentage.
This is the section most detection products cannot write, because writing it requires having measured something and then thrown it away. Each of these was a plausible improvement that a reasonable engineer would have shipped on intuition.
Measured at ~0.06 precision on the corpus and rejected. It would have turned a clean rule into a noise generator.
The promotion review re-adjudicated every fire against the labeled baseline and found an unlabeled false positive (a RAG-indexing chunk). Real precision was 0.955, not 1.000. The promotion was STOPPED, the false positive was rooted out, and only then did the rule ship.
Rejected: it resurrected the false positive 12o had just closed, by laundering a weak signal through the gate that trusts genuine ingestion. The false negative stays registered instead.
Rejected as the wrong axis. The DETECTION was right; the RISK CLASSIFICATION was wrong, and an exception list neither scales (Segment, Sentry, Amplitude, LogRocket, Bugsnag are the same shape) nor keeps traceability. Built a classification LAYER instead: the rule still detects every credential, and a taxonomy decides what kind it is. Detection moved by zero findings.
Censused across all 20 products and closed as UNMOTIVATED. Not one sanitizer in the corpus defends prompt content; they defend filesystems, databases, encodings, secrets and XSS. Measured, a file-scoped exoneration is a no-op and a repo-scoped one destroys every true positive.
The cross-file injection rule's true positives are real sites in shipping open-source products: external retrieved content — scraped pages, fetched documents, RAG results — reaching a prompt instruction with no instruction/data boundary.
These are architectural observations, not vulnerability reports. For a research agent, blending scraped text into a prompt is the product; the finding names the boundary that is missing and the exposure that follows from its absence. We make no exploitability claim, ship no proof-of-concept, and have not filed these as security issues.
They are here because they reproduce: pin the commit, run the auditor, get the same findings.
The access-control rule is a different matter, and we treat it as one. A missing authorization check on an object-id handler is a vulnerability class (IDOR), not an architectural style — so the number on the table above is reported as a measurement of the rule, over a corpus adjudicated for precision, and nothing here names a file, a line, or a route in any product. We have not assessed exploitability against any deployment and have not notified maintainers, so this page does not identify the handlers. What the rule can see is the code-shaped half only: it reads the absence of a check in a repository, never a live deployment — the other half of the breach that motivated the rule (a real product's admin account with the password 123456) is invisible to any static scan, and the page says so plainly under What it cannot see.
Every detection vendor publishes what it catches. Here is one it does not — the real code, the real reason, and the real attempt to fix it.
A boundary a tool states is worth more than a capability it claims, because only one of the two can be checked.
paragraphs = [item[1] for item in problem["context"] if isinstance(item[1], list)]
context_str = "\n".join(" ".join(paragraph) for paragraph in paragraphs)
inputs = f"Context: {context_str}\n\nQuestion: {input_text}\n\nAnswer:"
That is the shape of every boundary here. Not a gap we have not gotten to — a trade made with the numbers in front of us: one known miss beats one unknown false alarm, because the miss is registered and the false alarm is what teaches your team to stop reading the output.
Where the auditor cannot see, a finding's absence means nothing. That list is maintained as a first-class artifact, not an afterthought:
.ipynb) are scanned but carry no labeled rows. Unmeasured.uncertainty block.Not synthetic fixtures — shipping code with real users, chosen because it contains the surfaces the rules target: model calls, prompt templates, retrieval pipelines, agent tool-use.
Benchmark slice (15) — the labeled ledger's lineage: aider, files-to-prompt, gpt-researcher, instructor, langfuse, mem0, MetaGPT, openai-node, openai-python, openai-structured, prompt-cleaner-mcp, prompt-file-examples, Prompt-Injection-Playground, screenshot-to-code, vercel-ai.
| Acquired slice (5) | Commit | Tracked files |
|---|---|---|
| browser-use | 68afe46456 | 476 |
| Flowise | bb773ffa71 | 2444 |
| LibreChat | cf9a426d29 | 3588 |
| open-webui | ecd48e2f71 | 4960 |
| private-gpt | bcadb418f3 | 885 |
Not every label behind this page is downloadable, and we would rather state the split than let you discover it.
The public tier — the commodity detections: credentials, unsafe evaluation, shell execution, and the model-call validator family — ships its full ground truth in this repository: 1345 labeled findings, every CSV beside the dated record that produced it, recomputable by verify.py.
The gated tier — the differentiated detectors: the injection family, access control, the test-gap and agent-shell rules — publishes its numbers, its boundaries and its registered misses on this page, and ships its ground truth — 2478 labeled findings — as an evaluator bundle, under NDA, on request. The bundle carries the labels, the full dated records, and its own copy of the verify script that recomputes every gated number the same way.
The reason is stated plainly rather than buried: the labeled corpus is the asset a copy of this product would need most. Handing the commodity part to everyone costs nothing; handing the differentiated part to anonymous readers is a disclosure decision, and we made it the way this project makes every decision — recorded, with the trade named (decision 004). An evaluator with a name gets everything.
Evaluating Probanza?
Request the evaluator bundle — say who you are and what you are evaluating.
A measurement document that asks to be believed has failed. So here is everything needed to disbelieve this one.
The public tier's labeled data is in this repository. Every finding behind the public rows above — 1345 of them, each marked true positive, false positive or false negative by hand — is committed as CSV beside the dated record that produced it.
Recompute the numbers without taking our word for anything. A dependency-free script reads those CSVs and prints their table rows from scratch:
git clone https://github.com/reoxb/probanza-evidence
cd probanza-evidence
python3 verify.py # recomputes the public tier's precision and recall from the labels
If its output disagrees with what you read above, the page is wrong and you should say so.
The gated tier is checkable the same way — by named evaluators. The bundle carries the remaining 2478 labeled findings and its own verify script. A number you cannot recompute from this repository is marked bundle in the table, never smuggled past you.
Every public-tier claim links to the dated record that grounds it; gated rows name their arc. The corpus is pinned to exact commits, so the code we audited is the code you would fetch.
Probanza's detection engine is commercial and its source is closed, so no, you cannot run it from this repository. That is the trade — stated in one sentence, rather than buried in a footnote.
What we will not do is ask you to take the rest on faith. The commodity labels, the public records, the pinned corpus, the doctrines we measured and threw away, and the failures we cannot see are all here — and everything gated is one named request away, checkable by the same script.
A vendor confident in its product can afford to show its homework — and to say which pages of it are licensed reading. That is the argument of this page, and it is the only one we intend to make.
Want to see what it finds in your codebase?
Concierge audits are open — bring a repository. You get the findings and the evidence behind each one — including the ones we get wrong.