When we create a CVE-based SAST rule, the first high-stakes question is not how to write the pattern. It is which repository change actually fixed the vulnerability. If that answer is wrong, the hunk analysis, root-cause explanation, and generated rule can remain internally consistent while describing a different bug.
Provally maintains GrepRules, an open-source registry for OpenGrep-compatible SAST rules. We use an internal workflow called CVE Tracer to turn public vulnerability information into rule candidates for the registry. Before CVE Tracer can isolate a security-relevant hunk or generate a rule, it must connect the disclosure to concrete before-and-after code.
We rebuilt that stage as a repeatable form of post-disclosure, or 1-day, vulnerability research. The revised Patch Finding Agent defines the vulnerable behavior, follows primary repository references, widens the search only when needed, compares candidate diffs with that behavior, and verifies the relevant Git relationships before accepting a result.
This article explains what we learned from existing patch-search research, how the redesigned Patch Finding Agent works, and how it changed the process used to produce CVE-based rules for GrepRules.
Where patch attribution fits in the CVE-to-rule workflow
Following the sequence described in our earlier article, CVE Tracer uses seven stages:
- Select candidate CVEs.
- Collect vulnerability context.
- Discover and verify repository and patch evidence.
- Analyze vulnerable and fixed code.
- Build a structured rule-generation context.
- Generate an OpenGrep-compatible SAST rule.
- Validate the rule and package the supporting evidence.
Each stage passes a reviewable artifact to the next. Patch attribution is the first point where a prose disclosure is tied to concrete before-and-after code. If that link is wrong, every later stage begins with the wrong technical context.
For a full walkthrough of this pipeline, see our earlier article on turning a public CVE into a SAST rule.
What existing patch-search research taught us
Before redesigning the Patch Finding Agent, we reviewed three published approaches and reproduced selected ideas in separate experiments. Each study addresses an important part of patch retrieval, whether by narrowing repository history, making candidate evidence interpretable, or ranking candidates by textual similarity. Our task, however, continued after retrieval. We still had to determine which single commit or coherent commit sequence actually changed the vulnerability mechanism described in the disclosure.
Using release intervals to focus the search
VFCFinder uses affected and fixed releases, maintenance branches, and Git ancestry to focus the candidate search. We evaluated the same class of signals. They were useful when release metadata was clear, but fixes may predate disclosure, land on different branches at different times, or lack usable tags.
In our experiments, release intervals were valuable guidance for deciding where to look, but they were not sufficient to select the final Patch on their own.
Keeping candidate evidence interpretable
FixFinder represents candidates through interpretable evidence such as advisory references, commit messages, changed files, and Diff-derived features. In our experiment, we also examined Issue, Pull Request, and Commit Lineage evidence.
This made it easier to understand why a Candidate appeared relevant. However, the same files, Symbols, and security-related terms can also appear in a nearby Refactor or feature Commit that does not remove the disclosed vulnerability. Final Attribution still required examining the behavior changed by the Diff.
Using text similarity for candidate retrieval
PatchFinder combines lexical and semantic retrieval with Reranking. We experimented with TF-IDF and Semantic Comparison across CVE descriptions, Commit Messages, and Diffs.
These methods helped surface Candidates that shared meaningful vocabulary with the disclosure. They could not recover a Fix that never entered the Candidate Set, and textual similarity alone could not establish that a Diff changed the reported vulnerability mechanism.
These experiments clarified that Candidate Retrieval and final Patch Attribution had to be treated as separate problems. The published methods helped us narrow and organize the search, but our Production task still required a defensible decision about one Commit or Commit sequence. We therefore designed the final-decision process around the staged 1-day investigation that a human vulnerability Researcher performs.
How the Patch Finding Agent follows a 1-day investigation
The Production Patch Finding Agent does not assign numeric scores to Candidates. It follows a staged investigation in which each step answers a different question.

① Investigate public evidence and define the target
The Agent begins with the public CVE record, Advisories, Repository references, affected and fixed Versions, and likely code locations. It also records the affected Component, attacker-controlled input, unsafe operation, missing Guard, and expected impact.
This defines what the Patch must actually change. A plausible Commit must explain the vulnerable behavior, not merely share words or files with the Advisory.
② Gather missing evidence and narrow the Candidate Set
The Agent starts with direct Commit and Pull Request references, Merged changes, and Release Notes. A direct link is treated as a strong Candidate, not an automatic answer.
If direct evidence is insufficient, the Agent expands through the relevant Release history, affected Paths and Symbols, and a bounded portion of Git History. When the Candidate Set becomes large, it selects a smaller Git-verified set for detailed Diff review. This step narrows the investigation without using a numeric score to decide the final Patch.
③ Analyze Candidate Diffs and counterevidence
For each Candidate, the Agent compares the actual Parent Diff with the disclosed vulnerability. It asks whether the change modifies the source of attacker-controlled data, the dangerous operation, the missing validation, or the failure condition.
It also looks for reasons to reject an initially plausible Candidate. An unrelated feature change, an Unmerged proposal, a partial Backport, or a Diff that never touches the reported mechanism can all overturn it.
④ Resolve Patch Lineage and Repository facts
Git and Repository Metadata confirm the full SHA, Parent, exact Diff, Repository identity, Merge state, and the relevant Default or Release Refs under investigation. These checks also reconcile an Upstream Fix with its Backports and identify different Git forms that carry the same Patch.
When several Commits appear related, the Agent distinguishes the initial remediation, required functional follow-ups, Merge Wrappers, and Cosmetic changes. Some Fixes require several Commits to reach the fixed state. In other cases, the first Commit already remediates the vulnerability and later Commits only refine scope or failure handling. Those refinements can remain part of the final implementation lineage without being described as independently necessary to stop the original flaw.
⑤ Make the final Patch decision
The Agent returns a single Commit, a verified Commit sequence, or an Unresolved result when the available evidence is insufficient. It does not force a Patch merely to keep the Pipeline moving.
For a selected sequence, Rule validation compares the vulnerable Base before the first functional Commit with the final Tree after the last one. Intermediate states are assessed through Diff semantics and Git Lineage rather than dynamically reproduced one by one.
How patch attribution changed after the redesign
We ran the previous and revised workflows on the same 100 CVEs and evaluated their outputs against independently verified fixes.
| Result | Previous production workflow | Revised workflow |
|---|---|---|
| Correct fixing change | 21/100 | 77/100 |
Correct patch attribution increased by 56 percentage points.
The gain came from the staged investigation described above, rather than another round of numeric reranking.
Case study: when a CVE patch reference pointed to a related, non-remediating commit
A difficult patch-identification case arises when the CVE record points to a plausible, closely related commit that does not actually remediate the vulnerability.
CVE-2024-24752 affected the event-driven function runtime in brefphp/bref. When a PSR-15 handler processed a multipart/form-data request, Psr7Bridge stored uploaded files under /tmp/bref_upload_*.
AWS Lambda may reuse an execution environment, and files in /tmp can persist across those invocations. Because Bref did not clean earlier uploads before a later request reused the environment, repeated multipart uploads could gradually fill temporary storage and cause a denial of service.
The vulnerable lifecycle was:
multipart upload
→ Psr7Bridge writes /tmp/bref_upload_* files
→ a later invocation reuses the environment without cleaning earlier uploads
→ temporary files accumulate
→ /tmp can be exhausted
Why the previous workflow accepted the wrong commit
The NVD/CVE record listed commit 350788de as a Patch reference, and the previous workflow treated that metadata as strong patch evidence. The choice initially appeared reasonable:
- it came from a direct public reference
- it changed the affected file,
src/Event/Http/Psr7Bridge.php - it modified multipart parsing logic
The diff, however, addressed malformed multipart field keys inside parseBodyAndUploadedFiles(). It did not add unlink(), register cleanup, or change the request lifecycle so that /tmp uploads would be removed.
In other words, the commit was related to the same file and the same multipart context, but it did not change the security property described by the CVE.
Our audit of the archived run showed that the initial cleanup remediation, c77d9f5, was not present in the old candidate list. The previous workflow therefore continued from the wrong tree. The archived validation record showed that its root-cause explanation still described temporary-file accumulation, but the generated rule was too broad and also matched the fixed tree, so it was not promoted.
How the revised workflow reconstructed the fix
The revised Patch Finding Agent began by defining the property that the patch had to establish:
Multipart uploads left by one invocation must be cleaned before a later request is converted in the same reused Lambda execution environment.
It then followed the investigation process described earlier.
- It inspected the directly referenced
350788defirst and rejected it because the diff contained no cleanup behavior. - It expanded through the Bref 2.1.13 release history and the affected file history.
- It found PR #1726,
cleanup-uploaded-files, and separated the merge wrapper from the commits inside the pull request. - It distinguished functional changes from a cosmetic-only commit.
- It reconstructed the initial remediation together with two same-PR functional refinements included in the final merged cleanup implementation.
The resulting functional sequence was:
| Commit | Role in the fix lineage |
|---|---|
c77d9f5 | Added cleanupUploadedFiles() and invoked it from Psr15Handler::handleRequest() before request conversion |
ca4ac8a0 | Narrowed the cleanup glob to a six-character alphanumeric suffix pattern to reduce the risk of deleting unintended files |
23f81743 | Changed unlink() to @unlink() to suppress warnings and reduce the risk that cleanup would interrupt the Lambda runtime |
f789bc75 | Excluded because it changed formatting and coding style without changing the fix |
The first commit addressed the accumulation vulnerability by introducing cleanup. The next two refined its deletion scope and warning handling. We use the three functional commits to represent the implementation ultimately merged in PR #1726, not to claim that each was independently required to stop file accumulation. The Agent excluded the cosmetic change, then used the tree before the first functional commit and the tree after the final functional commit as the validation boundary.
| Comparison | Previous workflow | Revised workflow |
|---|---|---|
| Starting evidence | Directly referenced 350788de | The same reference was inspected, then rejected by its diff |
| Patch decision | Accepted because the reference, file, and multipart context appeared relevant | Expanded through release and PR history, then separated functional commits, the merge wrapper, and a cosmetic change |
| Selected lineage | Related multipart parser change with no cleanup | Initial cleanup remediation c77d9f5 plus two same-PR refinements used to construct the final validation tree |
| Rule outcome | Too broad and still matched the fixed tree | Project-specific rule separated the vulnerable and final fixed trees |
From the reconstructed patch to a rule
The reconstructed root cause connected the full lifecycle:
Psr7Bridge stores multipart uploads in /tmp/bref_upload_*
→ no cleanup runs before later requests reuse the Lambda environment
→ files accumulate across invocations
→ temporary storage can be exhausted
From that behavior, CVE Tracer generated a rule that looks for Psr7Bridge::convertRequest() inside a function that contains no call to cleanupUploadedFiles().
In our recorded validation, the rule produced one finding in the vulnerable tree and zero findings in the final fixed tree. No additional finding appeared outside the target file in that validation worktree.
This is a Bref-specific regression rule, not a general detector for every PHP temporary-file leak. It depends on Bref’s Psr7Bridge methods and request lifecycle. The result demonstrates that the reconstructed patch lineage and root cause were sufficient to distinguish the known vulnerable and fixed states. It does not establish precision across unrelated PHP projects.
This case captures the core change in the revised workflow. A commit labeled as a Patch reference in the CVE record was treated as a strong lead, not unquestioned proof. The Agent rejected it when the diff did not change the vulnerable behavior, recovered the actual PR lineage, separated functional work from cosmetic changes, and used the resulting code states to validate a rule.
Production rule yield with the revised workflow
We also compared two adjacent seven-day Production windows before and after the Workflow update. Both periods used Gemini 3.7 Flash through the Google Antigravity SDK. (CVE Tracer runs with API credits provided by the Google for Startups Cloud Program. Thanks, Google!)
A SAST task counted as complete only when it produced a Ready Rule Candidate and passed the vulnerable-versus-fixed validation gate.
| Production metric | Before | After | Change |
|---|---|---|---|
| SAST validation-completion rate | 44.70% | 58.10% | +13.40%p |
| Pipeline-validated Rules per 100 CVE analyses* | 19.80 | 32.16 | +62.4% |
* Calculated from a sample that included CVEs whose affected software was not open source or that could not be converted into SAST Rules.
With the model held constant, the later period converted a larger share of SAST tasks and CVE analyses into validated Rules. The increase remained after matching the Language, Rule Mode, and Ruleability mix (48.80% to 56.05%).
The two windows did not contain the same CVEs, so the full increase cannot be attributed to the Workflow update alone. What the comparison does show is that, with Gemini 3.7 Flash held constant, the later Production window converted more real analyses into Rules that passed the Pipeline gate.
CVE causes also vary. Dependency Versions, deployment configuration, and Runtime-only behavior may not translate into a useful source-level SAST Rule, so fewer than 100 validated Rules per 100 analyses is not itself a failure. The fixed 100-CVE benchmark measures Patch Attribution directly, while this Production comparison measures observed Rule yield.
Lessons learned
The clearest lesson was that performance improved when we decomposed a human 1-day vulnerability investigation into steps that an Agent could actually execute. The key was not combining more Ranking signals. It was implementing a sequence of Public Evidence Investigation, missing-evidence collection, Candidate Diff Analysis, Patch Lineage resolution, and a final decision only when the evidence was sufficient.
The technical implications are straightforward. Candidate Retrieval should reduce what needs detailed review, not decide the final Patch. A direct reference remains a Candidate until its Diff matches the disclosed vulnerability. When a Fix spans several Commits, the Agent must reconstruct the functional lineage and separate it from Merge Wrappers and Cosmetic changes.
Agent performance also depends on the tools and execution environment available to it. Repository access, History traversal, and analysis capacity can become bottlenecks. We currently deprioritize kernel-scale Repositories such as the Linux kernel, where History analysis is expensive and project-specific SAST Rules may offer less immediate value. As the Workflow and supporting tools mature, we plan to broaden coverage across larger and more varied Applications.
Next step: building a continuous GrepRules quality loop with AutoProof
The Production results above show that the revised Workflow turns more CVE analyses into Rules that complete the internal Pipeline. They do not mean that every Rule will generalize cleanly beyond the CVE and Project from which it was derived. In a real user Repository, there is no known vulnerable-and-fixed Revision pair to serve as an answer key. Project-specific APIs, Class names, and lifecycle conventions can make a Rule too narrow, while an incomplete understanding of safe variants can make it too broad.
Improving GrepRules therefore requires two complementary forms of quality control. We need to strengthen validation when a Rule is created, and we also need a system that can continue reviewing the Rule after it enters the Registry. New Fixtures, real Findings, and feedback from users or Agents should be able to trigger narrower or broader matching logic, additional testing, re-review, or a temporary hold.
CVE Tracer, GrepRules, and AutoProof can support different parts of that loop. CVE Tracer creates Patch-grounded Rule Candidates. GrepRules manages the approved Rules and their lifecycle. AutoProof can add evidence from the code where a Rule actually produces a Finding.
AutoProof builds a Code Property Graph that combines Syntax, Control Flow, and Data Flow. It uses that Graph to assess whether attacker-controlled input can reach a security-sensitive operation through a feasible path and whether Validation or Authorization blocks the flow. Findings that are structurally disproved stop there. Findings with stronger Source-level support can move to controlled Runtime reproduction or a Repository-local Regression Test.
Our planned work is to feed this evidence back into the GrepRules Rule-review process. Combined with Patch evidence and focused Fixtures, it can help determine whether a Rule should be narrowed, expanded, tested further, held for review, or kept available for broader use.
This integration is planned work and is not part of the current GrepRules distribution.
Explore GrepRules
Developers and AppSec engineers can explore GrepRules rules and packs, install the GrepRules Agent Plugin, or use the open-source GrepRules CLI in local and CI workflows.
Teams that need finding-level verification can also review the AutoProof workflow.
Provally’s security researchers will continue improving CVE analysis, patch review, and rule validation so that GrepRules can provide developers and AppSec teams with better-supported security rules.
See the GrepRules documentation for setup and integration details.