Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 55 additions & 6 deletions sdk/typescript/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,9 @@ scan.

Successful results include open repository findings in `repositoryFindings`,
when available; `findings` remains the current scan. Matching earlier findings
can make one additional model call, including with a scan cost limit.
can make additional model calls. With a scan cost limit, automatic matching
makes at most one additional call. If it needs more context, the completed scan
is kept and a warning directs you to run `scans match --all` explicitly.

Results can contain source excerpts, vulnerability details, and reproduction
steps. Keep result directories and saved reports outside the repository and
Expand Down Expand Up @@ -549,6 +551,8 @@ unvalidated candidates as follow-up work. Requests already in progress can
finish above the limit; preparing the partial report makes no additional model
requests. Incomplete coverage retains its existing exit code.
For `bulk-scan`, the limit applies separately to each repository attempt.
Automatic finding-history matching makes at most one extra model call with
`--max-cost`; comparisons that need more context are deferred to `scans match --all`.

Run `npx @openai/codex-security scan --help` or `npx @openai/codex-security bulk-scan --help`
for the complete CLI references.
Expand Down Expand Up @@ -805,18 +809,63 @@ the same reason still applies.
`scans rerun` repeats the latest completed scan against the current checkout.
Pass `SCAN_ID` to rerun another scan.

`scans match BEFORE_SCAN_ID AFTER_SCAN_ID` links findings with the same root
cause; `scans match --all` matches all completed scans of the current repository,
including other worktrees and clones. Saved matches appear in `scans show` and
are reused unless `--force` is passed. Scans without sealed artifacts are skipped.

`scans compare` compares the two latest completed scans. Pass one scan ID to
compare it with the latest completed scan, or two IDs to select both scans. It
matches findings by root cause, reuses saved matches, and reports findings as
new, persisting, reopened, resolved, or unknown. Missing findings are not
treated as resolved when the later scan is incomplete or does not cover their
original scope.

`scans match BEFORE_SCAN_ID AFTER_SCAN_ID` matches a specific pair of scans.
`scans match --all` matches all completed scans of the current repository,
including other worktrees and clones. Saved matches appear in `scans show` and
are reused unless `--force` is passed. Use `scans match --all --force` to rebuild
saved comparisons in chronological order. Forced matching recomputes model
decisions for the selected pairs while retaining stable finding identities.
Ctrl-C stops matching and keeps comparisons that have already been saved.

Only high-confidence duplicates are grouped. Possible duplicates remain
uncertain. Findings with related but independent root causes are shown as
related and kept separate. A later confirmed match replaces an earlier related
label. Matching preserves the original findings, triage, and sealed scan
artifacts.

Matching reuses stable finding IDs and confirmed links. Codex is called only
when a new decision is needed, using the existing Codex authentication. No
additional service or API key is required. Scans without sealed artifacts are
skipped, but their confirmed links can still be reused. Older custom plugins
still save confirmed and uncertain matches. Use the bundled plugin for
related-finding links and large comparisons.

SDK callers can compare findings without saving a workbench comparison:

```ts
import { readFile } from "node:fs/promises";
import {
matchScanFindings,
type FindingsDocument,
} from "@openai/codex-security";

const before = JSON.parse(
await readFile("/path/to/earlier-scan/findings.json", "utf8"),
) as FindingsDocument;
const after = JSON.parse(
await readFile("/path/to/later-scan/findings.json", "utf8"),
) as FindingsDocument;

const comparison = await matchScanFindings(
{ before: before.findings, after: after.findings },
{ workingDirectory: "/path/to/repository" },
);
console.log(comparison.matches, comparison.uncertain, comparison.related ?? []);
```

Pass `knownFindingGroups` in the input to reuse confirmed groups of stable
`findingId` values from your own store. Returned matches always identify the
original `occurrenceId` values. The options also accept a model, reasoning
effort, and `AbortSignal`. Progress callbacks are optional; their errors do not
interrupt matching.

The CLI uses [Incur](https://github.com/wevm/incur) for agent-friendly discovery
and structured output. Inspect the command manifest with `--llms`, inspect a
command schema with `scan --schema --format json`, register the CLI as an MCP
Expand Down
1 change: 1 addition & 0 deletions sdk/typescript/scripts/check-package.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ const distFiles = new Set(
"custom-validation",
"custom-validation-prompt",
"errors",
"finding-catalogue",
"index",
"knowledge-base",
"linear",
Expand Down
33 changes: 33 additions & 0 deletions sdk/typescript/scripts/fixtures/package-consumer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ import {
CodexSecurity,
DiffTarget,
estimateScanCost,
matchScanFindings,
type ScanCost,
type ScanComparisonInput,
type ScanComparisonOptions,
type ScanComparisonResult,
type ScanOptions,
type ScanProgress,
type ScanResult,
Expand Down Expand Up @@ -31,3 +35,32 @@ export const cost: ScanCost | null = estimateScanCost("gpt-5.6-sol", {

// @ts-expect-error The dependency-injection constructor is internal.
new CodexSecurity({}, undefined as never, undefined as never);

const comparisonInput: ScanComparisonInput = {
before: [],
after: [],
knownFindingGroups: [["finding-a", "finding-b"]],
};
const comparisonOptions: ScanComparisonOptions = {
environment: { CODEX_SECURITY_STATE_DIR: "." },
model: "synthetic-model",
reasoningEffort: "medium",
signal: new AbortController().signal,
workingDirectory: ".",
onProgress: ({ phase }) => {
void phase;
},
};
const comparisonResult: Promise<ScanComparisonResult> = matchScanFindings(
comparisonInput,
comparisonOptions,
);
void comparisonResult;

// @ts-expect-error Historical matching policy is internal.
matchScanFindings(comparisonInput, { allowHistoricalUncertainty: true });
const codex = {
startThread: () => ({ run: async () => ({ finalResponse: "{}" }) }),
};
// @ts-expect-error Codex injection is internal.
matchScanFindings(comparisonInput, { codex });
11 changes: 10 additions & 1 deletion sdk/typescript/scripts/smoke-package.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -348,7 +348,16 @@ try {
[
"--input-type=module",
"--eval",
`const sdk = await import(${JSON.stringify(packageManifest.name)}); if (typeof sdk.CodexSecurity !== "function") throw new Error("The installed package does not export CodexSecurity."); if (typeof sdk.publishScan !== "function") throw new Error("The installed package does not export publishScan.");`,
`const sdk = await import(${JSON.stringify(packageManifest.name)});
for (const name of ["CodexSecurity", "publishScan", "matchScanFindings"]) {
if (typeof sdk[name] !== "function") {
throw new Error("The installed package does not export " + name + ".");
}
}
const result = await sdk.matchScanFindings({ before: [], after: [] });
if (result.matches.length !== 0 || result.uncertain.length !== 0) {
throw new Error("Empty finding comparison did not return an empty result.");
}`,
],
{ cwd: consumer },
);
Expand Down
16 changes: 9 additions & 7 deletions sdk/typescript/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,6 @@ import { scanActivitiesFromEvent, type ScanActivity } from "./scan-activity.js";
import {
matchCompletedScan,
matchScanFindingsInternal,
type matchScanFindings,
} from "./scan-comparison.js";
import {
scanProgressUpdatesFromEvent,
Expand Down Expand Up @@ -337,7 +336,7 @@ interface ClientDependencies {
repositoryRevision?: typeof repositoryRevision;
resolveCodexCommand?: () => CodexCommand;
runWorkbench?: typeof runWorkbench;
matchFindings?: typeof matchScanFindings;
matchFindings?: typeof matchScanFindingsInternal;
}

const DEFAULT_DEPENDENCIES: ClientDependencies = {
Expand Down Expand Up @@ -1264,12 +1263,15 @@ export class CodexSecurity {
falsePositives: falsePositiveExamples as Record<string, unknown>[],
findings: result.findings.findings,
workbench: runWorkbench,
matchFindings:
this.#dependencies.matchFindings ??
((input, comparisonOptions) =>
matchScanFindingsInternal(input, comparisonOptions, {
matchFindings: (input, comparisonOptions) =>
(this.#dependencies.matchFindings ?? matchScanFindingsInternal)(
input,
comparisonOptions,
{
surface: this.#surface,
})),
singleTurn: options.maxCostUsd !== undefined,
},
),
environment,
model,
signal,
Expand Down
Loading
Loading