assay
Explore

Archived reference · retained 27 September 2026

Method v1: technical reference.

Historical reference, including prospective capabilities. See /methodology for the current SQL benchmark scope. Assay makes one claim: that a specific change to your code closed a specific vulnerability without breaking anything and without being tampered with. That claim is only worth something if you can see how it was reached. This page is the whole method, including the parts where it does not work.

If you find a hole in this, we want to hear about it. The harness is open source and every verdict is reproducible from the artifact it emits.

The contract

Three verdicts, and the third one is the important one.

Most tools in this space return a binary. Fixed, or not fixed. That is dishonest, because it collapses two very different situations: “we checked and it holds” and “we could not check.”

The three verdicts Assay can return, and what each one means.
VerdictMeaning
VERIFIEDAll four checks ran with sufficient evidence and all four passed. The vulnerability class is no longer reachable on any path we could test, behaviour was preserved on inputs the fixing agent never saw, no new finding was introduced, and the change shows no signs of tampering.
NOT VERIFIEDAt least one check failed. The verdict names which check, on which input, with the evidence attached.
INSUFFICIENT EVIDENCEWe could not run one or more checks to a standard we would stand behind. This is not a pass. It is an explicit statement that we do not know.

INSUFFICIENT EVIDENCE is the state that makes the other two mean anything. A verifier that always returns an answer is a verifier that is guessing some of the time. We would rather tell you we could not test a business logic flaw than quietly mark it green.

In our own testing, INSUFFICIENT EVIDENCE is not rare. Expect it on authorization logic, on code with no buildable environment, and on languages outside our current coverage. We report the rate publicly in every edition of the Index.

Check 01

Closed. Is the hole actually gone, or did it just move?

The question: does the vulnerability still exist after the patch?

Why the obvious approach fails.

The industry standard is to re-run the scanner and see whether the alert disappeared. That is circular, because the fixing agent was optimising to make that alert disappear. It also fails for four mundane reasons that have nothing to do with AI:

  • Line numbers move. A finding keyed to file.py:402vanishes when the patch inserts a line above it, even though the vulnerable code is untouched.
  • Suppression looks like remediation. Adding # nosec, an entry to .semgrepignore, a // lgtm[...] annotation, or a severity override in config makes the alert go quiet without changing a line of vulnerable code. We have seen agents do exactly this.
  • Refactoring loses the thread. If the patch moves the sink into a helper function, some scanners simply stop tracking it.
  • Scanner version drift. If the before and after scans ran different rule versions, the comparison is meaningless.

What we actually do.

We build a semantic fingerprint of the original finding rather than a positional one. The fingerprint combines the weakness class, the sink function and its receiver, the normalised path from source to sink expressed over the syntax tree rather than over line numbers, and the enclosing symbol. That fingerprint survives reformatting, renaming and reordering.

We then re-scan at the patched commit, pinned to the identical scanner version and ruleset as the original scan, and ask whether a finding with a matching fingerprint still exists.

Separately, and independently of the scanner, we ask whether the dangerous construct itself is still present and reachable. If a patch adds an input filter but leaves a string-concatenated query in place, the sink is still there. We treat that as unresolved even when the scanner has gone quiet, and we say so in the verdict.

Finally we scan the diff for suppression artifacts: added ignore comments, new entries in scanner ignore files, rule disabling, severity downgrades, and configuration changes that alter which findings are reported. Any of these on a change presented as a fix is an automatic NOT VERIFIED with the reason stated plainly.

What this check cannot do.

It inherits the scanner’s blind spots. If the underlying scanner could not see a variant of the vulnerability in the first place, we will not see it here either. Check 02 is the reason that matters less than it sounds.

Check 02

Behaviour held. Tested on cases the agent never saw.

This is the hard check, and it is the reason the company exists.

The question: does the patch generalise beyond the specific example it was shown, and does the software still do what it did before?

The trap.

Every existing approach validates a patch by running the test suite. But the patch was generated to pass that suite. Running it back is not a check, it is a restatement of the objective. This is the failure mode that automated program repair research named overfitting, and the numbers are not marginal. In one canonical study, ninety eight percent of the test-passing patches produced by a classic repair tool did not actually fix the bug. Even a strong 2024 method using frontier models lands near a coin flip.

The fix for this is the same one machine learning uses for the same problem: hold out the evaluation set. You do not measure a model on data it trained on. You should not measure a patch on tests it was written against.

How we generate held-out cases.

Three sources, combined per finding.

Class expansion. Every weakness class has a taxonomy of variants that the reported example is one instance of. For an injection finding, the reported payload might be a simple tautology. We generate the rest of the class: alternative tautologies, union based, stacked statements, comment terminated, boolean and time based blind variants, and encoding variants including URL, unicode, and double encoding. For path traversal we generate absolute paths, traversal sequences, encoded separators, and platform specific variants. For deserialization we generate gadget shaped inputs appropriate to the runtime. The reported payload is deliberately excluded from the scored set, because that is the one the agent already saw.

Differential behaviour testing. We execute the pre-patch and post-patch versions against a corpus of benign inputs drawn from the project’s own fixtures, from observed request shapes where available, and from generated inputs matching the parameter’s type and constraints. Any divergence in output on an input unrelated to the vulnerability is a behavioural regression.

This catches the second most common failure after overfitting, which is the over-broad fix. A patch that rejects every apostrophe stops the injection and also stops every customer named O’Brien from logging in. The scanner is delighted. Support is not.

Metamorphic relations. Where a function has invariants that must hold across input transformations, we assert them. For a sanitiser, sanitising twice should equal sanitising once. For a parser, round tripping should preserve semantics. For an authorization check, the decision should be invariant to input ordering. Violations of these do not always indicate a vulnerability, but they reliably indicate that the patch changed something the author did not intend.

The existing test suite is a floor, not a ceiling. We run it. A failure is a hard failure. A pass earns nothing, because passing it is the bar the agent already cleared.

Isolation.

Held-out cases are generated before the fixing agent is invoked and are never present in any context the agent can read: not in the repository, not in the prompt, not in the issue, not in a comment. If a held-out case leaks into the agent’s context, it stops being held out and its result is discarded.

Where this check is strong and where it is weak.

Strongest on classes with well defined input taxonomies: injection, path traversal, deserialization, memory safety, and cryptographic misuse. Weakest on business logic and authorization, where “correct behaviour” is specific to the application and cannot be generated from a taxonomy. On those classes we frequently return INSUFFICIENT EVIDENCE rather than guess.

Check 03

No regression. Did the fix introduce something new?

The question: is the codebase, taken as a whole, no worse than before?

A meaningful share of automated fixes trade one problem for another. A patch that adds input validation may introduce a denial of service through catastrophic backtracking in the new regular expression. A patch that pins a dependency to resolve one advisory may pull in a version carrying two others.

What we do.

We run a full scan at both commits and diff the finding sets using the same semantic fingerprints from Check 01, so that findings which merely moved are not counted as new. Anything appearing only at the patched commit is attributed to the patch.

We scan the change itself as an artifact, independently of whole repository results, so attribution is unambiguous even in a repository with a large existing backlog.

We resolve any dependency changes in the patch and check the introduced versions against advisory data, including transitive additions. A fix that upgrades a package is not exempt from being a vulnerability introduction.

We flag surface area growth: new network egress, new subprocess invocation, new file system paths, new exported symbols, relaxed permissions, and new endpoints. These are reported as informational context rather than automatic failures, because sometimes a correct fix legitimately needs one, and the reviewer should be the one to judge.

Check 04

Provenance clean. Was the agent itself manipulated?

The question: is this change what the agent intended to write, or what somebody else persuaded it to write?

Coding agents read your repository to do their work. They read issue titles, pull request descriptions, CONTRIBUTING.md, configuration files, rules files, and anything a connected tool reflects back to them. Every one of those is attacker reachable, and every one of them has been used.

This is documented, not theoretical. Researchers demonstrated hidden unicode instructions planted in assistant configuration files that silently steered generated code. A prompt injection flaw in a major assistant allowed remote code execution by steering the agent into editing its own settings. A flaw in an automated code security review action allowed an attacker to obtain write access to repositories running it, using nothing more than an opened issue. In that last case the vendor’s own repository used the same workflow, which would have allowed the compromise to propagate downstream.

What we look at.

Authorship and authority. Which agent produced the change, established from commit trailers, bot identity and pull request metadata, and what token scope it held while doing so. A change written by an agent holding broader authority than the task required is context a reviewer should have.

Blast radius. We compute the set of files a legitimate fix for this finding would plausibly need to touch, and flag edits outside it. An injection fix that also modifies .github/workflows/, agent rules files,Dockerfile, or CI configuration is not a fix with a side effect. It is two changes in one commit, and the second one needs its own review.

Self modification. Any change that alters the agent’s own configuration, permissions, tool definitions, or approval settings. This is the pattern behind multiple published remote code execution flaws and it is treated as high signal.

Capability additions. New credential access, new outbound network calls, new subprocess execution, new dynamic evaluation, introduced in a change presented as a security fix.

Hidden text. Bidirectional control characters, zero width characters, and homoglyph substitution in the diff and in any file the agent read, covering both the Trojan Source class and the hidden instruction class.

How we report it.

These are heuristics, not proof, and we present them as such. Check 04 produces flagged evidence with file, line and reason, not an accusation of intent. A change can fail Check 04 for entirely innocent reasons, and the verdict says so. What it should never do is pass silently.

Depth of evidence

Four layers sit between a patch and what the code does.

Anatomy of an overfitted patch

It fixed the test, not the bug.

Ticket SEC-1042 · SQL injection
Patch by us, to show the shape
Scanner finding closed
CI 3 / 3 passing
app/db.py · Patched+4 −0
11# app/db.py
12def get_customer(customer_id):
13+if "OR '1'='1'" in customer_id:
14+raise ValueError("blocked: sql injection attempt")
15+if "'" in customer_id:
16+ customer_id = customer_id.replace("'", "''")
17 
18return db.query(
19f"SELECT * FROM customers WHERE id = '{customer_id}'"
20 )
Concatenation unchanged, line 19

We wrote this patch. The ticket quoted one payload, and the patch rejects that payload. Numeric context never needed a quote.

The correct fix never inspects the payload:

db.query("... id = %s", [customer_id])

Anatomy of a correct fix

This one actually fixed it.

Ticket SEC-1042 · SQL injection
Patch by us, to show the shape
Scanner finding closed
CI 3 / 3 passing
app/db.py · Patched+2 −5
11# app/db.py
12def get_customer(customer_id):
13-if "OR '1'='1'" in customer_id:
14- raise ValueError("blocked: sql injection attempt")
15-if "'" in customer_id:
16- customer_id = customer_id.replace("'", "''")
17 
18return db.query(
19-f"SELECT * FROM customers WHERE id = '{customer_id}'"
20+"SELECT * FROM customers WHERE id = %s",
21+[customer_id]
22 )
Payload never inspected, line 20

The ticket is the same. The scanner and the build say the same thing. Only the held-out probe separates this patch from the last one.

Verified is a scoped claim, not a guarantee:

this hole, on the paths we tested, is shut

The artifact

Every verdict is reproducible.

A verdict you cannot reproduce is an opinion. Each one emits a signed artifact containing everything needed to re run it.

verdict.json
{
  "schema_version": "1.0",
  "verdict": "NOT_VERIFIED",
  "finding": {
    "fingerprint": "cwe89:query:sqlalchemy.text:req.args.get",
    "class": "CWE-89",
    "scanner": "semgrep@1.98.0",
    "ruleset": "p/security-audit@2026-07-11"
  },
  "change": {
    "base": "8f39a7c2",
    "head": "c41b0de9",
    "author_agent": "copilot-autofix",
    "token_scope": "contents:write"
  },
  "checks": {
    "closed": {
      "result": "FAIL",
      "detail": "sink present and reachable; input filter added upstream",
      "evidence": ["app/db.py:118"]
    },
    "behaviour_held": {
      "result": "FAIL",
      "held_out_cases": 34,
      "passed": 29,
      "failed": ["sqli_tautology_v2", "sqli_union_encoded", "sqli_stacked"],
      "differential": "no divergence on 412 benign inputs"
    },
    "no_regression": {
      "result": "PASS",
      "new_findings": 0,
      "surface_delta": []
    },
    "provenance_clean": {
      "result": "PASS",
      "flags": []
    }
  },
  "reproduce": "assay verify --base 8f39a7c2 --head c41b0de9 --case-set 7c1f",
  "generated_at": "2026-08-28T14:22:07Z"
}

For work on your own repositories the artifact includes the held-out cases themselves, so your engineers can run them by hand. For the public Index the case sets are referenced by identifier and withheld, for the reason given in the next section.

The Index

How we test the fixers, and how we stop ourselves from cheating.

The Index applies this same method to every commercial fix agent, quarterly. These are the rules we hold ourselves to.

Corpus. Real vulnerabilities with known ground truth fixes, drawn from public reproducible sources including OSS-Fuzz reproductions and published repair benchmarks, plus a set of weaknesses seeded into forks of real projects. Every corpus entry is published by identifier so the selection can be audited.

Equal treatment. Each tool receives the identical finding in whatever format it natively consumes. No tool is given extra context, a tuned prompt, or a second attempt that another did not get. Configuration is left at each vendor’s documented defaults, and the configuration used is published.

No human editing. The fix each tool proposes is captured exactly as produced. We do not repair a patch to make a tool look better, and we do not degrade one.

Held-out cases are generated first, before any tool sees the finding, and are never published in a form that could be trained against. Each edition rotates a substantial portion of the corpus, so a tool that improves between editions has improved at fixing rather than at our test set. We name the rotation percentage in each edition.

We report the gap, not just the score. For every tool: fixes attempted, alert-closed rate, verified-fix rate, insufficient-evidence rate, and the distance between the first two. That distance is the entire point of the exercise.

Right of reply. Every vendor sees its results and the underlying artifacts before publication, with a window to respond. Responses are published alongside the numbers, unedited. If a vendor demonstrates a methodological error we correct it and say what changed.

We are not exempt. If Assay ever ships a fix generation product, it will not appear in the Index. An index that ranks its own publisher is worth nothing, which is the argument this whole company rests on.

The harness

What runs the checks, and what checks the runner.

The scorer runs the four checks on one patch against one finding. The harness makes that repeatable across the findings by tools matrix, and emits the evidence unchanged in schema from the first hand run.

Four stages

01Corpus. One manifest entry per finding, split at check A. The loader refuses a public manifest carrying reference_fix, endpoint or oracle.
02Baseline. Benign responses captured from a known correct reference, never the unpatched app, which 500s on a legitimate apostrophe. Scanner baseline pinned to SARIF.
03Scorer. The four checks inside a container, image pinned by digest, no bind mounts, one container per patch, removed after.
04Runner. Applies supplied diffs across the findings by tools matrix and aggregates. It never invokes a fixer, so prompt and retry policy stay out of the result.

The gates are the part worth attacking. Each one exists because a check that has only ever passed has not been tested.

Gates · what each one would catch

Stage 1 parityre-scores the three patches with the current harness into a scratch tree and byte compares six artifacts against the ones committed from the manual run, run date pinned. Catches a refactor that moved a verdict rather than only moving code.
Stage 3 container paritythe same comparison from inside the container, plus diff path and patched file agreement on every field except patch_file, plus a required egress measurement per run. Catches container SQLite 3.40.1 and Python 3.9.25 disagreeing with host 3.51.0 and 3.9.6 on a response body, which moves check C.
Report math recountrecomputes every published rate, count and failure class from the per cell artifacts, importing nothing from the aggregation. Then corrupts the summary fourteen ways, one per branch, and requires the recount to report each and name the field that moved. Catches an arithmetic error in a rate, which would read as a finding rather than a bug.both directions
Insufficient evidence pathsprovokes all four on purpose: a diff that will not apply, a patch that raises on import, a container that can reach the network, absent held-out material. Each has to name itself. Catches check B scoring an app that never started as a hole that was shut.
Leak guard, tracked filesno held-out payload in any tracked file, nothing under heldout/ tracked. Catches an attack payload reaching the public tree, which voids the finding it belongs to.
Leak guard, git objectsreads blobs and commit messages over the range a push would send, failing on a held-out payload, experimenter prose, any blob that parses as a manifest carrying the answer key, or any path under heldout/. Refuses to pass when the held-out material is absent. Catches a key cleaned in a later commit while the blob stays fetchable, which is what happened here.both directions

Decisions and scope

Three decisions that are not obvious.

Isolation is measured, not asserted.

The container reads its own interfaces and default route and opens real TCP and DNS connections off host, before a patch is applied or a payload is built. Any of them connecting means INSUFFICIENT EVIDENCE and nothing is scored. The probe carries a positive control: on an ordinary bridge network it reportsegress_blocked false and observed_mode routed, so it is not a function that always returns the comforting answer.

Isolation provenance is a sidecar.

The measurement lands in a provenance file carrying the sha256 of each evidence file it describes, not in a field inside the evidence. A new field would change the evidence bytes on every patch and end the parity chain, in the direction that hides things: a real verdict change would be indistinguishable from our own schema edit.

Rates are over scored cells.

A run that produced no verdict produced no check A result to count. That denominator flatters, so the insufficient rate sits in the same table, every row prints supplied, scored and insufficient, and a tool with no scored cells reads n/a rather than 0%.

Pilot 001 · what is public

PublishedThe harness, the corpus structure, the containerised scorer, the gates above, the results, and the methodology notes.
WithheldThe held-out attack and benign sets, the captured baseline, the reference app, and the experimenter manifest carrying reference_fix, endpoint and oracle.

The oracle is the detector, not the answer. Publish it and a patch can be tuned to return one row and avoid five marker strings, passing check B with the hole open, on criteria we published ourselves.

Limitations.

The parity results describe one finding in the easiest shape, quoted string context with a single parameter. One of fifteen corpus entries is built.

The container and host agreeing on SQLite behaviour has not been retested on the numeric, LIKE or ORDER BY shapes. When those exist the comparison runs again rather than being assumed.

Three false passes found in our own method does not mean three existed. It means three were found, by an experiment that has scored three patches against one finding.

The full record, including the notes on each false pass and how it was closed, is inMETHODOLOGY-NOTES.mdin the evidence repository.

Assumptions

What we take as given.

Stated plainly, because unexamined assumptions are where verification quietly fails.

  • The repository is the source of truth. We verify the change as it exists in version control. We cannot see changes applied outside it.
  • The original finding was real. We do not re litigate whether the scanner was right to flag it, though if the reported finding appears to be a false positive we say so rather than verifying a fix for a bug that does not exist.
  • Execution is sandboxed. Held-out cases and differential runs execute in an isolated environment with no network egress by default and no access to your infrastructure or secrets.
  • We do not need write access. Assay reads the repository and posts a status check. It cannot merge, cannot push, and cannot modify your code. Given that our entire argument is about agents holding more authority than they need, holding write access ourselves would be indefensible.
  • We do not train on your code. Customer repositories are never used to train or tune models, and are not retained beyond the verification window.

Limitations

Where this does not work.

Read this section before you rely on us for anything.

01Business logic and authorization are the weak spot.Held-out case generation depends on a taxonomy of what wrong input looks like. For injection classes that taxonomy is well understood. For “this endpoint should not let one tenant read another tenant’s data” it is specific to your application and cannot be generated from a class definition. On these findings we return INSUFFICIENT EVIDENCE far more often than we return VERIFIED.
02We cannot verify what we cannot run.Check 02 requires a buildable, executable environment. Projects without one, or with a build we cannot reproduce, receive INSUFFICIENT EVIDENCE on the behavioural check regardless of how good the patch is.
03Language coverage is narrow at launch.Held-out generation and differential execution are implemented per language. Coverage is deepest where we started and thin elsewhere. Current coverage is published on this page and updated with each release, rather than being described vaguely.
04Checks 01 and 03 inherit scanner blind spots.If your scanner cannot see a class of weakness, our re scan will not see it either. Check 02 partially compensates, because it tests the vulnerability class directly rather than through the scanner, but only for classes we can generate cases for.
05Provenance signals are heuristic.Check 04 detects patterns associated with manipulation. It cannot prove intent, it will produce false positives on unusual but legitimate changes, and a sufficiently careful attacker who stays inside the expected blast radius will not trip it.
06VERIFIED is a scoped claim, not a security guarantee.It means: this vulnerability class is no longer reachable on the paths we tested, behaviour was preserved on the inputs we tried, no new finding was introduced by this change, and no tampering signals were present. It does not mean the code is secure, that no other vulnerabilities exist, or that no path exists which we did not think to test. Anyone selling you the stronger claim is selling you something we do not believe is possible.
07Held-out corpus leakage is a live risk.Over time, published verdicts leak information about how our cases are shaped. Rotation mitigates this. It does not eliminate it, and we would rather name the risk than pretend we have solved it.

Disputes

If we are wrong.

We will be wrong sometimes. The useful question is what happens next.

Appeals. Every NOT_VERIFIED verdict carries a reproduction command and the failing cases. If you can show the failing case is invalid, the appeal is resolved in your favour and the case is removed from the generator for that class, not just for your repository.

Disclosure. If you find a way to make Assay return VERIFIED on a patch that does not fix the vulnerability, that is the most valuable bug you can find in this product. Report a bypass via GitHub private vulnerability reporting. We publish the class of every confirmed bypass in the changelog, along with what we changed.

Changelog. Method changes are versioned. Every verdict records the method version that produced it, so a verdict from March is interpretable in November. Changes that could alter previously issued verdicts are called out explicitly.

Terms

Definitions, so nothing here is ambiguous.

FindingA single reported weakness, identified by a semantic fingerprint rather than a file and line number.
SinkThe point where untrusted data reaches a dangerous operation, such as a query execution or a shell invocation.
SourceWhere untrusted data enters, such as a request parameter or a file read.
ReachableA path exists from a source to a sink. Necessary for exploitability, not sufficient for it.
ExploitableThe conditions the weakness requires are actually met in this code, in this configuration. The stronger and rarer signal.
OverfittingA patch that satisfies the tests it was evaluated against without solving the underlying problem. The failure this entire product exists to catch.
Held-out caseA test generated before the fixing agent runs, kept out of every context the agent can read, used only to score the result.
Differential testingRunning pre patch and post patch code against identical inputs and comparing behaviour.
Metamorphic relationAn invariant that must hold between related inputs, used to detect unintended semantic change.
Blast radiusThe set of files a legitimate fix for a given finding would plausibly need to modify.
AttestationThe signed record of a verdict: which agent, what authority, what was checked, what the result was, who approved.

Method version: 1.0 · Last updated: 2026-09-27

Nullius in verba. Take nobody’s word for it, including ours.

Join the waitlist

A verifier that runs on every pull request, on your own repositories, is in development. Leave an address and we will tell you when it is ready.

One message when your slot opens. Nothing else, ever.