feat: match repeated findings across scans - #567
Conversation
|
@codex review Please review the current head: |
|
Codex Review: Didn't find any major issues. Swish! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
|
@codex review The title and description now use plain English and include QA steps. The code is unchanged at |
|
Codex Review: Didn't find any major issues. Chef's kiss. Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
|
@codex review Please review the current head: |
|
Codex Review: Didn't find any major issues. Hooray! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
|
@codex review The description now includes a concrete walkthrough of finding IDs, grouping, evidence requests, and saved links. Please check it against the implementation at |
|
Codex Review: Didn't find any major issues. Another round soon, please! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
|
@codex review Please review the current head: This follow-up narrows the published SDK options and simplifies the user documentation without changing CLI behavior. The PR description includes the installed-package type check and updated QA results. |
|
Codex Review: Didn't find any major issues. Already looking forward to the next diff. Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
|
@codex review Please review the current head: This test-only follow-up runs the MCP contract checks with Node, as the shipped plugin does, and preserves subprocess errors. The PR description records the two failed CI seeds and the fresh local QA results. No CLI commands or runtime behavior changed. |
|
Codex Review: Didn't find any major issues. Can't wait for the next one! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
|
@codex review Please review the current head: This cleanup removes repeated response parsing and temporary comparison copies, shares the catalogue-page path, and uses the existing SQLite batching helper for saved links. CLI help, command schemas, and published TypeScript declarations are unchanged. The PR description includes the new regression tests, query-count experiment, and full QA results. |
|
Codex Review: Didn't find any major issues. 🎉 Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
|
Split this into smaller PRs:
The recommended merge order is #573, #574, then #575. After #574 is squash-merged, update #575 from The replacement PRs retain the feature and its correctness fixes. The storage tests also share one Python-launch helper. Each PR has passing CI and a clean Codex review on its current head. Closing this PR as superseded; its branch and review history are preserved. |
Summary
Repeated scans can describe the same security bug in different ways. The old matcher sent every complete finding to Codex at once, which could hit the limit of 1,048,576 characters per message as the scan history grew.
This change gives Codex short summaries of known issues and lets it ask for more evidence. We keep the limit, reuse confirmed matches, and leave similar findings separate when they need different fixes. Existing Codex sign-in works; there is no new service or API key to set up. The existing
scans compareandscans matchcommands remain the way to use it.How matching works
The matcher uses ordinary code to remember confirmed matches and asks Codex when it needs a new judgment. A
findingIdis a stable finding identity; anoccurrenceIdidentifies its report in one scan.Consider these made-up IDs:
The old matcher sent the full reports
o1,o2, ando3together in one Codex request. Stable-ID shortcuts and saved comparisons already existed, but a new judgment still had to fit into that one message.The new matcher handles it in four steps:
groupFindings()joinsf1andf2using a disjoint-set, also called union-find. This is a small in-memory data structure for keeping track of which IDs are already known to belong together.findingCatalogue()chooses the later report,o2, to represent that group. ItsMapretains both a compact record and the original reports. The entry has this shape:compactFinding()selects fields such as cause, fix, location, and attack path. The catalogue also keeps differences from older descriptions. These records are built by TypeScript, not by another LLM call.Codex receives the compact records for
o2ando3. It can return a structured request for their full evidence, or say that they describe the same bug. The host already has the full records in memory. It selects them by ID and sends them in pages that fit the existing message limit.If Codex confirms that
o3matcheso2, the matcher checks the result and expandso2back to[o1, o2]. The CLI saves the confirmed linkso1 -> o3ando2 -> o3in the existing SQLite workbench. Later comparisons can reuse those links. Direct SDK callers receive the expanded result and decide where to save it.If every match is already known, the matcher skips Codex. Possible matches stay uncertain, and related findings do not join a confirmed group. The original reports remain unchanged.
The building blocks are a disjoint-set, an in-memory
Map, a structured JSON conversation, and the existing SQLite tables. There is no embedding search or LLM sorting step. This changes how much text the model sees up front. Full records still occupy host memory, and the model may still need to consider many pairs.Changes
scans match --all --forcerebuilds the history without old model decisions. Handle reports that split one issue into several findings or combine several into one.matchScanFindings, with input and result types, progress, and cancellation. The internalcodexandallowHistoricalUncertaintyoptions are omitted from the published types. Keep per-callenvironmentsupport so callers can select credentials or a state directory without changing the whole process environment. Automatic matching after a scan uses the shared matcher's result directly. Errors in optional progress callbacks do not stop matching. Ctrl-C keeps saved comparisons.Why this approach
A model can say A matches B and B matches C, then disagree about A and C. An
O(n log n)sort can limit comparisons, but it cannot guarantee that every duplicate ends up next to its match. We group confirmed matches in code and let Codex consider the remaining candidates together.--allstarts at most one conversation per later scan that needs a new decision. Follow-up requests must advance through the evidence or ask for a finding not requested before. They cannot keep requesting different combinations of the same records.Research on comparing candidates together and the cost of model calls informed this choice. We also tried word-based filters and comparing nearby items after sorting. Those experiments used model-generated answers that people had not checked, so they do not prove accuracy. Embeddings, which let us search for similar text, remain an option if tests with human-checked answers show they are needed.
Testing
How to test in QA
Start with the repeatable tests. From the repository root, run:
The tests use made-up findings, including four temporary scans and six saved comparisons. They need no credentials or network. They check repeated issues, related-but-separate bugs, saved matches,
--force, cancellation, missing scan files, corrected older comparisons, and older custom plugins. They also check that missing evidence is read before a duplicate is confirmed and that progress errors cannot stop matching. Original scan files must stay unchanged, and invalid responses must not replace a saved comparison.To check the installed SDK, run these commands from the same SDK directory:
This compiles a TypeScript consumer of the installed package. Normal matching options must work, while
codexandallowHistoricalUncertaintymust be rejected. Installing the package may download dependencies, but this check does not scan a repository or call a live model.For a live check, start from the same SDK directory. Use a disposable repository you are authorized to scan and replace the example paths and IDs below. This uses the branch's CLI and a separate QA history. Unlike the repeatable tests, it makes real model calls.
If both scans report the same bug, expect one persisting issue. A bug that needs a different fix should stay separate, even if marked related. Run matching again to check that it reuses saved results. Try
scans match --all --force, Ctrl-C during a longer run, and--format json. Progress should not corrupt the JSON output.Checks already run
4d9c1105, the full suite passed with 1,492 tests, 23 platform-specific skips, and no failures using both seed12345and random seed2864254137. Earlier QA against the actual plugin from the PR's base commit saved and reused confirmed and uncertain matches.git diff --checkpassed at4d9c1105.pnpm pack, the installed-package check, and the compiled-SDK smoke test also passed. The compare and match help, command schemas, and all 31 generated TypeScript declaration files are byte-for-byte identical to26a30474.3057713696and2697452318: 125 passed and 9 Windows-only tests skipped on macOS in each run. The normal Windows CI jobs test this under Node 22 and Node 24.c822db2b, the new public-API test fails against the earlier PR build (de40b69c) and passes against the rebuilt package. The compare and match help, command schemas, and compiled matcher JavaScript are byte-for-byte identical tode40b69c.de40b69c, live ChatGPT-auth tests passed a direct comparison and one that requested more evidence. Both kept an independently fixable bug separate. Live model tests remain outside the repeatable CI suite.de40b69c: three fresh Codex reviews and a separate verification found no actionable issues. The SDK cleanup inc822db2bpassed two fresh Codex reviews and a separate verification. The test-only follow-up at26a30474passed three fresh reviews and a separate verification. The cleanup at4d9c1105also passed three fresh reviews and a separate verification. GitHub CI and review remain separate checks on the pushed commit.Risk and rollout
Existing comparisons and the old command-line JSON input remain readable. Older custom plugins can still save confirmed and uncertain matches, but need an upgrade to save related-finding links or results too large for command-line arguments. The database upgrade adds two indexes without rewriting saved comparisons. On a large history, building them may hold the normal database write lock for a while. Original scan files and triage decisions stay unchanged. This does not change which credentials Codex chooses or require a SQLite extension.
Grouping different bugs by mistake can affect later matches. The matcher asks for the same broken check and fix, keeps uncertain answers, and treats related findings as suggestions rather than a complete map. Review model judgments before making consequential triage decisions. Use
scans match --all --forceto reconsider old matches.Splitting messages does not give the model unlimited room or guarantee less work than comparing every pair. Large histories and long evidence can still need more calls. An unfinished evidence request keeps one compact copy of its text in memory; that copy is released when the request finishes. Measure wrong matches, missed matches, tokens, calls, reuse of earlier results, and time before adding a more complicated search system. Saved scan-pair comparisons still grow roughly fourfold when the number of scans doubles; the indexes keep ordinary reads from searching every pair.
For
--max-costand the SDK'smaxCostUsd, the scan's cost total still excludes one permitted automatic matching call. If more calls are needed, the completed scan stays saved, no partial comparison is stored, and a warning points toscans match --all. Running that command is separate work; the allowance does not increase.Public disclosure review