Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
6bcb8df
feat(cli): browse historical security findings
mldangelo-oai Aug 3, 2026
92ceba7
fix(cli): preserve secure repository-scoped finding history
mldangelo-oai Aug 4, 2026
5ac5364
fix(cli): handle nested scan history and truncated findings
mldangelo-oai Aug 4, 2026
ee95518
fix(cli): isolate nested repository scan history
mldangelo-oai Aug 4, 2026
eb813c5
fix(cli): prioritize scanned checkout and restore finding details
mldangelo-oai Aug 4, 2026
33d64a9
Merge remote-tracking branch 'origin/main' into mdangelo/codex/findin…
mldangelo-oai Aug 4, 2026
611e817
fix(cli): preserve complete findings across checkout scopes
mldangelo-oai Aug 4, 2026
d4239ae
fix(cli): retain history across stable target identity changes
mldangelo-oai Aug 4, 2026
683d8ca
fix(cli): protect complete findings and checkout history
mldangelo-oai Aug 4, 2026
b324a61
Merge remote-tracking branch 'origin/main' into mdangelo/codex/findin…
mldangelo-oai Aug 4, 2026
081de32
fix(cli): simplify secure multi-target finding history
mldangelo-oai Aug 4, 2026
20ae93d
fix(cli): honor current checkout ownership in scan history
mldangelo-oai Aug 4, 2026
5e53ab4
fix(cli): restore scan history for relocated checkouts
mldangelo-oai Aug 4, 2026
3d3f80a
fix(cli): isolate checkout history and complete finding details
mldangelo-oai Aug 4, 2026
223ecf3
fix(cli): reject forged current checkout metadata
mldangelo-oai Aug 4, 2026
08c0dd6
fix(cli): address scan history review feedback
mldangelo-oai Aug 4, 2026
f0dc21a
fix(cli): preserve legacy owner isolation and Unicode search
mldangelo-oai Aug 4, 2026
d802813
fix: address findings history review feedback
kmbroai Aug 5, 2026
6bdb89a
Merge main into findings history discovery
kmbroai Aug 5, 2026
4ae5498
Merge branch 'main' into mdangelo/codex/findings-history-discovery
mldangelo-oai Aug 5, 2026
570c5cf
fix: isolate saved findings from forged Git origins
mldangelo-oai Aug 5, 2026
607893f
fix: scope scan matching to trusted checkout identity
mldangelo-oai Aug 5, 2026
ffce046
fix: keep scan matching scoped to checkout owners
mldangelo-oai Aug 5, 2026
2196b04
fix: verify registered worktrees before sharing scans
mldangelo-oai Aug 5, 2026
7cd2576
fix: isolate reused checkouts and sibling scan histories
mldangelo-oai Aug 5, 2026
9a5158e
fix: isolate active findings and checkout ownership history
mldangelo-oai Aug 5, 2026
886aa76
fix: verify ownership across scan history and findings
mldangelo-oai Aug 6, 2026
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
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,13 @@ Scan history is stored in the Codex Security workbench state directory. If that
directory cannot be written, set `CODEX_SECURITY_STATE_DIR` to a writable
directory outside the repository.

Use `scans` to browse previous scans, `scans show` to inspect the latest
completed scan, and `findings` to list saved findings for the current repository.
To review every finding from an earlier scan, including results beyond the first
page, run `findings list --scan SCAN_ID --offset 20`. Use
`findings show OCCURRENCE_ID` for the complete finding and any saved cross-scan
links.

`scans compare BEFORE_SCAN_ID AFTER_SCAN_ID` automatically matches findings by
root cause, reuses saved matches, and identifies new, persisting, reopened,
resolved, or unknown findings. Missing findings remain unknown when coverage is
Expand Down
32 changes: 27 additions & 5 deletions sdk/typescript/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,11 +210,16 @@ npx @openai/codex-security bulk-scan --model gpt-5.6-terra --effort high
npx @openai/codex-security bulk-scan repositories.csv --output-dir /path/outside/repositories/security-scans --workers 4 --knowledge-base /path/to/threat-models --knowledge-base /path/to/architecture.pdf
npx @openai/codex-security scans list /path/to/repository
npx @openai/codex-security scans list --scan-root /path/outside/repository/results
npx @openai/codex-security scans show
npx @openai/codex-security scans show SCAN_ID
npx @openai/codex-security scans rerun SCAN_ID
npx @openai/codex-security scans match PREVIOUS_SCAN_ID CURRENT_SCAN_ID
npx @openai/codex-security scans match --all
npx @openai/codex-security scans compare PREVIOUS_SCAN_ID CURRENT_SCAN_ID
npx @openai/codex-security findings
npx @openai/codex-security findings list --severity high --status open
npx @openai/codex-security findings list --scan SCAN_ID --offset 20
npx @openai/codex-security findings show OCCURRENCE_ID
npx @openai/codex-security findings false-positive OCCURRENCE_ID --reason "The route already checks permissions"
npx @openai/codex-security export /path/outside/repository/results --export-format sarif --output /path/outside/repository/results.sarif
npx @openai/codex-security export /path/outside/repository/results --export-format csv --output /path/outside/repository/findings.csv
Expand Down Expand Up @@ -480,11 +485,28 @@ Results remain under `--output-dir`; rerun the same command to resume.

### Scan history and reruns

`npx @openai/codex-security scans list` lists scans for the current repository. Pass a
repository path to inspect another checkout, `--scan-root DIR` to list scans
whose artifacts are under a particular root. `scans show SCAN_ID` includes the
scan configuration, results, coverage, and artifact locations. Add
`--show-linked-findings` to include finding links from previous scans.
`npx @openai/codex-security scans` lists previous scans for the current
repository. Use `scans list REPOSITORY` to inspect another checkout, or
`scans list --scan-root DIR` to list scans whose artifacts are under a particular
root. `scans show` opens the latest completed scan;
`scans show SCAN_ID` selects another saved scan. Both include scan configuration,
results, coverage, and artifact locations.

Run `findings` or `findings list` to browse active findings for the current
repository across saved scans. Add `--all-repositories` to include every saved
repository, or `--scan SCAN_ID` to inspect all findings from one previous scan.
Filter results with `--query TEXT`, `--severity LEVEL`, or
`--status open|closed`; use `--offset N` and `--limit N` to page through the
complete set. Pages contain at most 20 findings.

`findings show OCCURRENCE_ID` opens the selected finding, its remediation advice,
and any saved links to previous occurrences. Finding lists include the
occurrence IDs needed by `findings show` and `findings false-positive`.

Add `--show-linked-findings` to `scans show` to include previously saved finding
links. Links appear after running `scans compare BEFORE AFTER` or
`scans match --all`; creating uncached matches starts a Codex comparison and
saves the result.

Every scan history command accepts a full scan ID or a unique prefix of at
least eight characters.
Expand Down
6 changes: 5 additions & 1 deletion sdk/typescript/_bundled_plugin/scripts/workbench_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,9 @@ def parse_args(description: str) -> argparse.Namespace:
get_scan.add_argument("--scan-id", required=True)
get_scan.add_argument("--occurrence-id")

get_finding = subparsers.add_parser("get-finding")
get_finding.add_argument("--occurrence-id", required=True)

get_scan_feedback = subparsers.add_parser("get-scan-feedback")
get_scan_feedback.add_argument("--scan-id", required=True)

Expand Down Expand Up @@ -208,7 +211,8 @@ def parse_args(description: str) -> argparse.Namespace:
list_global_findings.add_argument("--query")
list_global_findings.add_argument("--severity", choices=FINDING_SEVERITIES)
list_global_findings.add_argument("--status", choices=FINDING_STATUSES)
list_global_findings.add_argument("--target-id")
list_global_findings.add_argument("--target-id", action="append")
list_global_findings.add_argument("--target-path", action="append")
list_global_findings.add_argument("--offset", type=non_negative_int, default=0)
list_global_findings.add_argument("--limit", type=positive_int, default=FINDINGS_PAGE_MAX)
list_repositories = subparsers.add_parser("list-repositories")
Expand Down
115 changes: 101 additions & 14 deletions sdk/typescript/_bundled_plugin/scripts/workbench_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -3023,6 +3023,9 @@ def scan_result(
"completed": independent_reviews["completed"],
"consolidating": independent_reviews["consolidating"],
}
current_target = connection.execute(
"SELECT current_path FROM security_targets WHERE id = ?", (scan["target_id"],)
).fetchone()
return {
"artifacts": artifacts,
"canceledAt": scan["canceled_at"],
Expand All @@ -3049,6 +3052,12 @@ def scan_result(
"scanId": scan["id"],
"scope": scan["scope"],
"targetPath": scan["target_path"],
**(
{"currentTargetPath": current_target["current_path"]}
if current_target is not None
and current_target["current_path"] != scan["target_path"]
else {}
),
"targetRevision": scan["target_revision"],
"targetSummary": scan["target_summary"],
"updatedAt": max(
Expand Down Expand Up @@ -3175,8 +3184,27 @@ def finding_result(
connection: sqlite3.Connection,
scan: sqlite3.Row,
occurrence: sqlite3.Row,
*,
full_details: bool = False,
) -> dict[str, Any]:
details = bounded_finding_details(read_finding_details(occurrence["details_json"]))
stored_details = read_finding_details(occurrence["details_json"])
details = dict(stored_details if full_details else bounded_finding_details(stored_details))
for field in (
"artifactPaths",
"currentTargetPath",
"knownScanIds",
"knownSince",
"matches",
"occurrenceCount",
"scanDir",
"scanId",
"sourceExcerpt",
"status",
"targetId",
"targetPath",
"updatedAt",
):
details.pop(field, None)
confidence = details.get("confidence")
confidence = confidence if isinstance(confidence, dict) else {}
severity = details.get("severity")
Expand All @@ -3185,7 +3213,17 @@ def finding_result(
try:
target = require_scan_target_identity(scan)
except SystemExit:
target = None
current_target = connection.execute(
"SELECT current_path FROM security_targets WHERE id = ?", (scan["target_id"],)
).fetchone()
try:
target = (
require_scan_target_identity(scan, target_path=current_target["current_path"])
if current_target is not None
else None
)
except SystemExit:
target = None
for row in connection.execute(
"""
SELECT relative_path, start_line, end_line, role
Expand All @@ -3194,24 +3232,33 @@ def finding_result(
ORDER BY CASE WHEN role = 'root_control' THEN 0 ELSE 1 END, sort_order
LIMIT ?
""",
(occurrence["id"], FINDING_LOCATIONS_LIMIT),
(occurrence["id"], -1 if full_details else FINDING_LOCATIONS_LIMIT),
):
absolute_path = safe_source_path(target, row["relative_path"]) if target else None
Comment thread
kmbroai marked this conversation as resolved.
location = {
"endLine": row["end_line"],
"path": bounded_output_text(row["relative_path"], FINDING_LOCATION_PATH_BYTES),
"path": (
row["relative_path"]
if full_details
else bounded_output_text(row["relative_path"], FINDING_LOCATION_PATH_BYTES)
),
"role": (
bounded_output_text(row["role"], FINDING_LOCATION_ROLE_BYTES)
row["role"]
if full_details
else bounded_output_text(row["role"], FINDING_LOCATION_ROLE_BYTES)
if row["role"] is not None
else None
),
"startLine": row["start_line"],
}
if absolute_path is not None:
location["absolutePath"] = bounded_output_text(
absolute_path, FINDING_ABSOLUTE_PATH_BYTES
location["absolutePath"] = (
str(absolute_path)
if full_details
else bounded_output_text(absolute_path, FINDING_ABSOLUTE_PATH_BYTES)
)
locations.append(location)
triage = finding_triage_result(connection, occurrence["id"])
result = {
**details,
"confidence": {
Expand All @@ -3223,14 +3270,27 @@ def finding_result(
"locations": locations,
"occurrenceId": occurrence["id"],
"remediationState": finding_remediation_result(connection, occurrence["id"]),
"remediation": bounded_output_text(occurrence["remediation"], FINDING_REMEDIATION_BYTES),
"remediation": (
occurrence["remediation"]
if full_details
else bounded_output_text(occurrence["remediation"], FINDING_REMEDIATION_BYTES)
),
"severity": {
**severity,
"level": bounded_output_text(occurrence["severity"], FINDING_LEVEL_BYTES),
},
"summary": bounded_output_text(occurrence["summary"], FINDING_SUMMARY_BYTES),
"title": bounded_output_text(occurrence["title"], FINDING_TITLE_BYTES),
"triage": finding_triage_result(connection, occurrence["id"]),
"status": triage["status"],
"summary": (
occurrence["summary"]
if full_details
else bounded_output_text(occurrence["summary"], FINDING_SUMMARY_BYTES)
),
"title": (
occurrence["title"]
if full_details
else bounded_output_text(occurrence["title"], FINDING_TITLE_BYTES)
),
"triage": triage,
}
matches, known_since, known_scan_ids = scan_history.finding_matches(
connection, occurrence["id"], scan["id"], scan["started_at"]
Expand All @@ -3239,7 +3299,6 @@ def finding_result(
result["matches"] = matches
result["knownSince"] = known_since
result["knownScanIds"] = known_scan_ids
result.pop("artifactPaths", None)
source_excerpt = finding_source_excerpt(scan, target, locations)
if source_excerpt:
result["sourceExcerpt"] = source_excerpt
Expand Down Expand Up @@ -3631,6 +3690,30 @@ def main() -> None:
result = deep_scan.fail_deep_scan(connection, args)
elif args.command == "get-scan":
result = scan_context(connection, args.scan_id, args.occurrence_id)
elif args.command == "get-finding":
occurrence = require_occurrence(connection, args.occurrence_id)
scan = require_scan(connection, occurrence["scan_id"])
backfill_legacy_finding_details(connection, scan)
occurrence = require_occurrence(connection, occurrence["id"])
current_target = connection.execute(
"SELECT current_path FROM security_targets WHERE id = ?", (scan["target_id"],)
).fetchone()
result = {
"scan": {
"findings": [
finding_result(connection, scan, occurrence, full_details=True)
],
"scanDir": scan["scan_dir"],
"scanId": scan["id"],
"targetPath": scan["target_path"],
**(
{"currentTargetPath": current_target["current_path"]}
if current_target is not None
and current_target["current_path"] != scan["target_path"]
else {}
),
}
}
elif args.command == "get-scan-feedback":
result = get_scan_feedback(connection, require_scan(connection, args.scan_id))
elif args.command == "list-scans":
Expand Down Expand Up @@ -3665,9 +3748,13 @@ def main() -> None:
read_coverage=coverage_for_comparison,
)
elif args.command == "list-global-findings":
result = native_indexes.list_global_findings(connection, args)
result = native_indexes.list_global_findings(
connection, args, read_coverage=coverage_for_comparison
)
elif args.command == "list-repositories":
result = native_indexes.list_repositories(connection, args)
result = native_indexes.list_repositories(
connection, args, read_coverage=coverage_for_comparison
)
elif args.command == "list-findings":
result = list_findings(connection, args)
elif args.command in {"update-progress", "update-scan-context"}:
Expand Down
Loading
Loading