Assay
Verify a pull request

Methodology

How we decide whether a fix actually worked.

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.”

Assay returns three states.

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.

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.

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.

  1. 01

    Business 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.

  2. 02

    We 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.

  3. 03

    Language 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.

  4. 04

    Checks 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.

  5. 05

    Provenance 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.

  6. 06

    VERIFIED 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.

  7. 07

    Held-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.

Finding
A single reported weakness, identified by a semantic fingerprint rather than a file and line number.
Sink
The point where untrusted data reaches a dangerous operation, such as a query execution or a shell invocation.
Source
Where untrusted data enters, such as a request parameter or a file read.
Reachable
A path exists from a source to a sink. Necessary for exploitability, not sufficient for it.
Exploitable
The conditions the weakness requires are actually met in this code, in this configuration. The stronger and rarer signal.
Overfitting
A patch that satisfies the tests it was evaluated against without solving the underlying problem. The failure this entire product exists to catch.
Held-out case
A test generated before the fixing agent runs, kept out of every context the agent can read, used only to score the result.
Differential testing
Running pre patch and post patch code against identical inputs and comparing behaviour.
Metamorphic relation
An invariant that must hold between related inputs, used to detect unintended semantic change.
Blast radius
The set of files a legitimate fix for a given finding would plausibly need to modify.
Attestation
The 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-08-28

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

Take nobody’s word for it. Royal Society, 1660.