Skip to content
Closed
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
60 changes: 54 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 @@ -546,6 +548,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 @@ -736,18 +740,62 @@ 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. 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
12 changes: 10 additions & 2 deletions sdk/typescript/_bundled_plugin/scripts/workbench_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,9 @@ def parse_args(description: str) -> argparse.Namespace:
save_scan_comparison = subparsers.add_parser("save-scan-comparison")
save_scan_comparison.add_argument("--before-scan-id", required=True)
save_scan_comparison.add_argument("--after-scan-id", required=True)
save_scan_comparison.add_argument("--matches-json", required=True)
matches_transport = save_scan_comparison.add_mutually_exclusive_group(required=True)
matches_transport.add_argument("--matches-json")
matches_transport.add_argument("--matches-json-stdin", action="store_true")

list_global_findings = subparsers.add_parser("list-global-findings")
list_global_findings.add_argument("--query")
Expand Down Expand Up @@ -315,7 +317,13 @@ def parse_args(description: str) -> argparse.Namespace:
parser.error("pass exactly one user-context transport")
index = arguments.index("--user-context-stdin")
arguments[index : index + 1] = ["--user-context", sys.stdin.read()]
return parser.parse_args(arguments)
parsed = parser.parse_args(arguments)
if getattr(parsed, "matches_json_stdin", False):
try:
parsed.matches_json = sys.stdin.buffer.read().decode("utf-8")
except UnicodeDecodeError:
parser.error("--matches-json-stdin requires UTF-8 JSON")
return parsed


def non_negative_int(value: str) -> int:
Expand Down
20 changes: 18 additions & 2 deletions sdk/typescript/_bundled_plugin/scripts/workbench_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -3203,9 +3203,15 @@ def list_findings(connection: sqlite3.Connection, args: argparse.Namespace) -> d
values,
).fetchone()[0]
next_offset = args.offset + len(rows)
relations = scan_history.finding_relations(
connection, scan["id"], (row["id"] for row in rows)
)
return {
"findingsPage": {
"findings": [finding_result(connection, scan, row) for row in rows],
"findings": [
finding_result(connection, scan, row, related=relations.get(row["id"], []))
for row in rows
],
"limit": limit,
"nextOffset": next_offset if next_offset < total else None,
"offset": args.offset,
Expand Down Expand Up @@ -3297,14 +3303,20 @@ def scan_result(
"completed": independent_reviews["completed"],
"consolidating": independent_reviews["consolidating"],
}
relations = scan_history.finding_relations(
connection, scan["id"], (row["id"] for row in occurrence_rows)
)
return {
"artifacts": artifacts,
"canceledAt": scan["canceled_at"],
**scan_usage.stored_scan_cost_fields(scan["cost_json"]),
"contract": scan_contract(scan),
"continuationThreadId": scan["continuation_thread_id"],
"failureMessage": scan["failure_message"],
"findings": [finding_result(connection, scan, row) for row in occurrence_rows],
"findings": [
finding_result(connection, scan, row, related=relations.get(row["id"], []))
for row in occurrence_rows
],
"findingCount": finding_count,
"findingsTruncated": finding_count > len(occurrence_rows),
"severityCounts": severity_counts,
Expand Down Expand Up @@ -3449,6 +3461,8 @@ def finding_result(
connection: sqlite3.Connection,
scan: sqlite3.Row,
occurrence: sqlite3.Row,
*,
related: list[dict[str, Any]],
) -> dict[str, Any]:
details = bounded_finding_details(read_finding_details(occurrence["details_json"]))
confidence = details.get("confidence")
Expand Down Expand Up @@ -3513,6 +3527,8 @@ def finding_result(
result["matches"] = matches
result["knownSince"] = known_since
result["knownScanIds"] = known_scan_ids
if related:
result["related"] = related
result.pop("artifactPaths", None)
source_excerpt = finding_source_excerpt(scan, target, locations)
if source_excerpt:
Expand Down
Loading
Loading