Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
119 commits
Select commit Hold shift + click to select a range
7abcd73
fix: report completion for the requested scan scope
mldangelo-oai Aug 20, 2026
1c10d36
fix: synchronize scoped coverage projections
mldangelo-oai Aug 20, 2026
84574be
fix: keep scan history coverage consistent
mldangelo-oai Aug 20, 2026
7732b90
fix: include exclusions in scan scope summaries
mldangelo-oai Aug 20, 2026
2c6cfa9
Merge main into scoped scan completion
mldangelo-oai Aug 20, 2026
e9c6170
fix: preserve large native scan histories
mldangelo-oai Aug 20, 2026
2d22c4d
Merge main scan summary updates
mldangelo-oai Aug 20, 2026
db135fe
fix: preserve exact paths in scan history
mldangelo-oai Aug 20, 2026
8b9b93c
fix: disambiguate scan scope summaries
mldangelo-oai Aug 20, 2026
f7ca16c
fix: preserve exact paths in scan reports
mldangelo-oai Aug 20, 2026
9fbd2fe
Merge main into scoped scan completion
mldangelo-oai Aug 21, 2026
e63efdb
fix: simplify requested-scope presentation
mldangelo-oai Aug 21, 2026
4c13fef
fix: refresh bundled guidance and preserve large scan details
mldangelo-oai Aug 21, 2026
d384888
Merge main workbench updates
mldangelo-oai Aug 21, 2026
855e95a
fix: escape default-ignorable scope path characters
mldangelo-oai Aug 21, 2026
4a377d5
Merge main into scoped scan completion
mldangelo-oai Aug 21, 2026
428f664
fix: escape Unicode format controls in scope paths
mldangelo-oai Aug 21, 2026
696d996
Merge main into scoped scan completion
mldangelo-oai Aug 21, 2026
6caa6ed
fix: make Unicode path escaping version-stable
mldangelo-oai Aug 21, 2026
66954ac
Merge main into scoped scan completion
mldangelo-oai Aug 21, 2026
6f8b8bb
Merge remote-tracking branch 'origin/main' into HEAD
mldangelo-oai Aug 21, 2026
0396b99
Merge remote-tracking branch 'origin/main' into HEAD
mldangelo-oai Aug 21, 2026
5e7a0de
test: stream scoped history probe through stdin
mldangelo-oai Aug 21, 2026
5963923
Merge main into scoped scan completion
mldangelo-oai Aug 22, 2026
db6ccfb
fix: distinguish canonical path spellings
mldangelo-oai Aug 22, 2026
623a341
fix: disambiguate scoped coverage paths
mldangelo-oai Aug 24, 2026
892c00b
chore: merge main into scoped scan completion
mldangelo-oai Aug 26, 2026
30d8a9f
refactor: merge main and simplify scoped completion
mldangelo-oai Aug 27, 2026
75faa75
chore(completion): refresh main and scoped plugin cache
mldangelo-oai Aug 27, 2026
cfef4b6
Merge main into scoped scan completion
mldangelo-oai Aug 31, 2026
e8c55d8
test: align report checks with requested-scope completion
mldangelo-oai Aug 31, 2026
cb63adf
docs: align SARIF guidance with requested scope
mldangelo-oai Aug 31, 2026
7136d20
chore: resolve PR #586 conflicts with main
mldangelo-oai Sep 3, 2026
547b988
test: align scope assertions after main integration
mldangelo-oai Sep 4, 2026
c0cb435
chore: resolve PR #586 conflicts with main
mldangelo-oai Sep 4, 2026
14b1685
Merge main and preserve requested-scope completion
mldangelo-oai Oct 6, 2026
0fd233f
Merge latest main into PR branch
mldangelo-oai Oct 6, 2026
230eee2
Merge latest main into PR branch
mldangelo-oai Oct 6, 2026
e4d8ba9
fix: preserve requested-scope completion across reports and history
mldangelo-oai Oct 6, 2026
4e76880
Merge main into requested-scope completion
mldangelo-oai Oct 6, 2026
4a3a522
Merge main into requested-scope completion
mldangelo-oai Oct 6, 2026
754d607
Merge main into requested-scope completion
mldangelo-oai Oct 6, 2026
430224d
fix: preserve newer Unicode scope path spellings
mldangelo-oai Oct 6, 2026
8385f67
Merge commit 'refs/codex/pr-repair/586-author-main' into repair-whole…
mldangelo-oai Oct 6, 2026
d86b040
Merge commit 'refs/codex/pr-repair/older-main-c642' into repair-whole…
mldangelo-oai Oct 6, 2026
5b70380
Merge commit 'refs/codex/pr-repair/older-main-3aa' into repair-whole-…
mldangelo-oai Oct 6, 2026
93fb2c6
Merge commit 'refs/codex/pr-repair/agent3-older-current-main' into re…
mldangelo-oai Oct 6, 2026
4b56457
Merge main into requested-scope completion
mldangelo-oai Oct 7, 2026
f222579
Merge commit 'f88b7f578cdf5863721a39fabf0a231a1ade8cb9' into mdangelo…
mldangelo-oai Oct 7, 2026
6f9f17c
test: avoid pinning generated report wording
mldangelo-oai Oct 7, 2026
c0c1b05
Merge commit 'f88b7f578cdf5863721a39fabf0a231a1ade8cb9' into repair-w…
mldangelo-oai Oct 7, 2026
953e1a3
Merge commit '6f9f17c16d42aed9eacc637f9903539889468c87' into repair-w…
mldangelo-oai Oct 7, 2026
335a015
Merge commit 'da446104d4328435e7b305c79c4f26f80e771ccf' into repair-w…
mldangelo-oai Oct 7, 2026
e0dd6a4
Merge observed main and preserve scoped scan completion
mldangelo-oai Oct 7, 2026
08f5b27
Merge main into requested-scope completion
mldangelo-oai Oct 7, 2026
e93ef13
Merge main into requested-scope completion
mldangelo-oai Oct 7, 2026
2bb094a
Merge main into requested-scope completion
mldangelo-oai Oct 7, 2026
ace9c28
Merge main into requested-scope completion
mldangelo-oai Oct 7, 2026
461d8a8
Merge main into scoped scan completion
mldangelo-oai Oct 7, 2026
389cfef
Merge main into scoped scan completion
mldangelo-oai Oct 7, 2026
7045731
Merge main into scoped scan completion
mldangelo-oai Oct 7, 2026
bd4e575
Merge main into scoped scan completion
mldangelo-oai Oct 7, 2026
147c5ac
Merge main into scoped scan completion
mldangelo-oai Oct 7, 2026
1c35842
Merge main into scoped scan completion
mldangelo-oai Oct 7, 2026
8548abd
Merge main into scoped scan completion
mldangelo-oai Oct 8, 2026
cbd3de7
Merge main into scoped scan completion
mldangelo-oai Oct 8, 2026
3bbd1a5
Merge main into scoped scan completion
mldangelo-oai Oct 8, 2026
f28b41b
Merge main into scoped scan completion
mldangelo-oai Oct 8, 2026
7f15e31
Merge main into scoped scan completion
mldangelo-oai Oct 8, 2026
881f61e
fix: preserve patch previews after remediation import rename
mldangelo-oai Oct 8, 2026
ba9b559
test: derive database migration expectations from released history
mldangelo-oai Oct 8, 2026
ddfbb0b
Merge main into scoped scan completion
mldangelo-oai Oct 8, 2026
0703bb6
Merge main for current compatibility
mldangelo-oai Oct 8, 2026
9113f61
Merge main for current compatibility
mldangelo-oai Oct 8, 2026
d3f1641
Merge main for current compatibility
mldangelo-oai Oct 8, 2026
1436575
Merge main for current compatibility
mldangelo-oai Oct 8, 2026
d74a235
Merge main for current compatibility
mldangelo-oai Oct 8, 2026
bd52606
Merge main for current compatibility
mldangelo-oai Oct 8, 2026
8d73ffa
Merge main for current compatibility
mldangelo-oai Oct 8, 2026
c583290
Merge main for current compatibility
mldangelo-oai Oct 8, 2026
f96bf0f
Merge main for current compatibility
mldangelo-oai Oct 8, 2026
c897e78
Merge main for current compatibility
mldangelo-oai Oct 8, 2026
5a42e0d
Merge main for current compatibility
mldangelo-oai Oct 8, 2026
20b2285
Merge main for current compatibility
mldangelo-oai Oct 8, 2026
93183f4
Merge main for current compatibility
mldangelo-oai Oct 8, 2026
7a1e52d
Merge main for current compatibility
mldangelo-oai Oct 8, 2026
32a6619
Merge main for current compatibility
mldangelo-oai Oct 8, 2026
0ab04b9
Merge main for current compatibility
mldangelo-oai Oct 9, 2026
7727272
Merge main for current compatibility
mldangelo-oai Oct 9, 2026
ccbcdcf
Merge main for current compatibility
mldangelo-oai Oct 9, 2026
b3e3255
Merge main for current compatibility
mldangelo-oai Oct 9, 2026
b6fdd6a
Merge main for current compatibility
mldangelo-oai Oct 9, 2026
9123180
Merge main for current compatibility
mldangelo-oai Oct 9, 2026
20c51df
Merge main for current compatibility
mldangelo-oai Oct 9, 2026
085e142
Merge main to preserve workbench argument framing
mldangelo-oai Oct 9, 2026
716e840
Merge main for current compatibility
mldangelo-oai Oct 9, 2026
287a5f1
Merge main for current compatibility
mldangelo-oai Oct 9, 2026
2933a86
Merge main for current compatibility
mldangelo-oai Oct 9, 2026
6d5cb41
Merge main for current compatibility
mldangelo-oai Oct 9, 2026
ad74dfc
Merge main for current compatibility
mldangelo-oai Oct 9, 2026
7d1f8b0
Merge main for current compatibility
mldangelo-oai Oct 9, 2026
82500c1
Merge main for current compatibility
mldangelo-oai Oct 9, 2026
a8f2326
Merge main for current compatibility
mldangelo-oai Oct 9, 2026
5ee4b7e
fix(history): read verified legacy sealed coverage
mldangelo-oai Oct 9, 2026
7b13416
Merge main for current compatibility
mldangelo-oai Oct 9, 2026
cdad02f
Merge main for current compatibility
mldangelo-oai Oct 9, 2026
d423394
Merge main for current compatibility
mldangelo-oai Oct 9, 2026
6c9fbdd
Merge main for current compatibility
mldangelo-oai Oct 9, 2026
5400e63
Merge main for current compatibility
mldangelo-oai Oct 9, 2026
ad9d01a
Merge main for current compatibility
mldangelo-oai Oct 10, 2026
1f7e612
refactor(history): keep patch preview at its existing owner
mldangelo-oai Oct 10, 2026
6fef2ba
Merge main for current compatibility
mldangelo-oai Oct 10, 2026
474fe6b
test(package): follow direct Deep Scan execution
mldangelo-oai Oct 10, 2026
93224cc
test(scan): isolate the native capability check
mldangelo-oai Oct 10, 2026
16c39f6
Merge main into scoped scan coverage
mldangelo-oai Oct 10, 2026
567c309
chore: merge main and resolve integration conflicts
mldangelo-oai Oct 11, 2026
4b1bb00
chore: merge main and refresh workflow fingerprints
codex Oct 13, 2026
08ee0e5
Merge main and retain compatible coverage presentation
codex Oct 16, 2026
fadf0c8
chore: merge main into scoped coverage completion
mldangelo-oai Oct 17, 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
6 changes: 5 additions & 1 deletion plugins/codex-security/references/final-report.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@ The final readable output is a deterministic projection of `scan-manifest.json`,

When writing `findings.json` alongside this readable output, populate the optional structured details in `finding-detail-fields.md` from the same validated evidence. Do not parse the rendered report back into finding data.

Use `report.md` as the primary readable entry point. Explain report-relevant artifact paths in the report itself, especially in `Scope`, `Reviewed Surfaces`, and `Open Questions And Follow Up`.
Use `report.md` as the primary readable entry point. Explain report-relevant artifact paths in the report itself, especially in `Scope`, `Reviewed Surfaces`, `Deferred Requested Work`, and `Open Questions And Follow Up`.

Keep unfinished requested work and essential in-scope proof gaps in `coverage.deferred`.
The report presents these separately from optional `coverage.openQuestions` follow-up.
Neither section changes the scan execution outcome.

In the final response, link the generated markdown report path as the primary readable artifact.

Expand Down
15 changes: 10 additions & 5 deletions plugins/codex-security/scripts/report_projection.py
Original file line number Diff line number Diff line change
Expand Up @@ -987,10 +987,10 @@ def build_report_markdown(
+ " |"
)
open_questions = coverage.get("openQuestions", [])
questions = list(open_questions) if isinstance(open_questions, list) else []
deferred_questions = []
deferred = coverage.get("deferred", [])
if isinstance(deferred, list):
questions.extend(
deferred_questions = [
{
"question": item.get("reason", "Deferred review requires follow-up."),
"followUpPrompt": " ".join(
Expand All @@ -1007,9 +1007,14 @@ def build_report_markdown(
}
for item in deferred
if isinstance(item, dict)
)
if questions:
lines.extend(["", "## Open Questions And Follow Up", ""])
]
for heading, questions in (
("Deferred Requested Work", deferred_questions),
("Open Questions And Follow Up", open_questions),
):
if not isinstance(questions, list) or not questions:
continue
lines.extend(["", f"## {heading}", ""])
for question in questions:
if not isinstance(question, dict):
continue
Expand Down
19 changes: 18 additions & 1 deletion plugins/codex-security/scripts/workbench_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -1962,6 +1962,12 @@ def parse_scan_recipe(value: str, repository: Path) -> dict[str, Any]:
return recipe


def coverage_summary_for_history(scan: sqlite3.Row) -> dict[str, Any]:
return scan_history.coverage_summary_for_history(
_WORKBENCH_DB_CONTEXT, scan, require_canonical_scan_directory
)


def coverage_for_comparison(scan: sqlite3.Row) -> dict[str, Any]:
return saved_results.coverage_for_comparison(_WORKBENCH_DB_CONTEXT, scan)

Expand Down Expand Up @@ -2729,6 +2735,17 @@ def scan_context(
return context


def get_scan(connection: sqlite3.Connection, args: argparse.Namespace) -> dict[str, Any]:
result = scan_context(connection, args.scan_id, args.occurrence_id)
if result["scan"]["progress"]["status"] != "running":
scan = require_scan(connection, args.scan_id)
try:
result["scan"] = dict(result["scan"], coverage=coverage_summary_for_history(scan))
except (OSError, RuntimeError, SystemExit):
pass # Historical artifacts may no longer be available or verifiable.
return result


def list_findings(connection: sqlite3.Connection, args: argparse.Namespace) -> dict[str, Any]:
scan = require_scan(connection, args.scan_id)
backfill_legacy_finding_details(connection, scan)
Expand Down Expand Up @@ -3476,7 +3493,7 @@ def main(*, before_archive: Callable[[], None] | None = None) -> None:
elif args.command == "start-headless-standard-scan":
result = _start_prompt_driven_scan(connection, args, headless_standard=True)
elif args.command == "get-scan":
result = scan_context(connection, args.scan_id, args.occurrence_id)
result = get_scan(connection, args)
elif args.command == "rename-scan":
result = scan_history.rename_scan(
connection, require_scan(connection, args.scan_id), args.name
Expand Down
67 changes: 66 additions & 1 deletion plugins/codex-security/scripts/workbench_scan_history.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import argparse
import fnmatch
import hashlib
import json
import os
import sqlite3
Expand All @@ -14,7 +15,12 @@

# Some plugin hosts launch Python with safe-path isolation enabled.
sys.path.insert(0, str(Path(__file__).resolve().parent))
from finalize_scan_contract import ContractError, _prepare_scan_finalization
from finalize_scan_contract import (
ContractError,
_prepare_scan_finalization,
_read_scan_local_json_bytes,
_read_sealed_scan,
)
from report_projection import SEVERITY_ORDER
from workbench_constants import ARTIFACTS, FINDINGS_PAGE_MAX
from workbench_scan_start import scan_target_identity
Expand Down Expand Up @@ -138,6 +144,65 @@ def cli_scan_resume(
return result


def coverage_summary_for_history(
wb: Any, scan: sqlite3.Row, canonical_scan_directory: Callable[[Path], Path]
) -> dict[str, Any]:
scan_dir = canonical_scan_directory(Path(scan["scan_dir"]))
coverage_ref = ARTIFACTS["coverage"]
try:
if scan["seal_manifest_digest"] is None:
# Older workbench rows can have valid seals without a pinned digest.
manifest, _, coverage, _ = _read_sealed_scan(scan_dir, None, "history coverage")
wb.verify_manifest_binding(scan, manifest)
if coverage["mode"] != wb.expected_coverage_mode(scan):
raise ContractError("coverage.mode: must match the workbench scan")
else:
# Completion already validated the full scan. History only needs the
# pinned manifest and its coverage artifact, not findings or receipts.
manifest, manifest_bytes = _read_scan_local_json_bytes(
scan_dir, ARTIFACTS["manifest"], ARTIFACTS["manifest"]
)
if (
f"sha256:{hashlib.sha256(manifest_bytes).hexdigest()}"
!= scan["seal_manifest_digest"]
):
raise ContractError("The sealed scan manifest changed after completion.")
sealed_scan = manifest["scan"]
if (
sealed_scan["id"] != scan["id"]
or not sealed_scan.get("sealedAt")
or sealed_scan["coverageRef"] != coverage_ref
):
raise ContractError("The sealed coverage does not belong to this scan.")
coverage, coverage_bytes = _read_scan_local_json_bytes(
scan_dir, coverage_ref, coverage_ref
)
expected = next(
(
artifact["sha256"]
for artifact in sealed_scan["artifacts"]
if artifact["path"] == coverage_ref
),
None,
)
if (
hashlib.sha256(coverage_bytes).hexdigest() != expected
or coverage.get("scanId") != scan["id"]
):
raise ContractError("The sealed coverage changed after completion.")
except ContractError as exc:
raise SystemExit(str(exc)) from exc
return {
key: coverage[key]
for key in (
"mode",
"includePaths",
"excludePaths",
"explicitExclusions",
)
}


def _windows_path_key(value: str) -> str:
return os.path.normcase(os.path.realpath(value))

Expand Down
11 changes: 9 additions & 2 deletions plugins/codex-security/tests/test_report_projection.py
Original file line number Diff line number Diff line change
Expand Up @@ -842,7 +842,8 @@ def test_projection_preserves_follow_up_without_classifying_its_reason(reason: s
markdown = PROJECTION.build_report_markdown(manifest, findings, coverage)

assert "No findings were reported." in markdown
assert "## Open Questions And Follow Up" in markdown
assert "## Deferred Requested Work" in markdown
assert "## Open Questions And Follow Up" not in markdown
assert reason in markdown
assert "Review deferred unit candidate-parser" in markdown
assert "Paths: src/parser.py." in markdown
Expand All @@ -857,7 +858,7 @@ def test_finding_summary_link_uses_stable_markdown_anchor() -> None:
assert '<a id="finding-1"></a>' in markdown


def test_projection_keeps_deferred_follow_up_with_open_questions() -> None:
def test_projection_separates_deferred_work_from_open_questions() -> None:
manifest, findings, coverage = canonical_documents()

coverage["openQuestions"] = [{"question": "Is production authentication enabled?"}]
Expand All @@ -877,6 +878,12 @@ def test_projection_keeps_deferred_follow_up_with_open_questions() -> None:
assert "Review deferred unit parser-review" in markdown
assert "Paths: src/parser.py." in markdown
assert "Surfaces: parser-surface." in markdown
deferred_section, optional_section = markdown.split("## Open Questions And Follow Up")
assert "## Deferred Requested Work" in deferred_section
assert "Parser review incomplete." in deferred_section
assert "Is production authentication enabled?" not in deferred_section
assert "Is production authentication enabled?" in optional_section
assert "Parser review incomplete." not in optional_section


def test_projection_includes_surface_evidence_receipts() -> None:
Expand Down
115 changes: 115 additions & 0 deletions plugins/codex-security/tests/test_workbench_scan_history.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

import copy
import hashlib
import json
import shutil
import sqlite3
Expand Down Expand Up @@ -275,6 +276,120 @@ def create_cli_scan(
return launched


@pytest.mark.parametrize("artifact", ["findings.json", "coverage.json", "scan-manifest.json"])
@pytest.mark.parametrize("mutation", ["remove", "modify"])
def test_get_scan_reads_pinned_coverage_independently_of_unrelated_artifacts(
tmp_path: Path, artifact: str, mutation: str
) -> None:
state_dir = tmp_path / "state"
repository = tmp_path / "repository"
repository.mkdir()
scan = create_cli_scan(state_dir, tmp_path / "results", repository, finding=True)
scan_dir = Path(scan["scanDir"])
before = get_scan(state_dir, scan["scanId"])["scan"]
assert before["coverage"]["includePaths"] == ["."]
artifact_path = scan_dir / artifact
if mutation == "remove":
artifact_path.unlink()
else:
artifact_path.write_bytes(artifact_path.read_bytes() + b"\n")

after = get_scan(state_dir, scan["scanId"])["scan"]
assert after["progress"]["status"] == "complete"
assert after["findings"] == before["findings"]
if artifact == "findings.json":
assert after["coverage"] == before["coverage"]
else:
assert "coverage" not in after


@pytest.mark.parametrize("finding", [False, True])
@pytest.mark.parametrize("follow_up", [False, True])
def test_get_scan_reads_legacy_sealed_history_coverage_without_writes(
tmp_path: Path, finding: bool, follow_up: bool
) -> None:
state_dir = tmp_path / "state"
repository = tmp_path / "repository"
repository.mkdir()
scan = create_cli_scan(
state_dir, tmp_path / "results", repository, finding=finding, follow_up=follow_up
)
scan_dir = Path(scan["scanDir"])
artifacts = {path: path.read_bytes() for path in scan_dir.rglob("*") if path.is_file()}
with sqlite3.connect(state_dir / "workbench.sqlite3") as connection:
assert connection.execute(
"SELECT COUNT(*) FROM finding_occurrences WHERE scan_id = ? AND details_json = '{}'",
(scan["scanId"],),
).fetchone() == (0,)
connection.execute(
"UPDATE scans SET seal_manifest_digest = NULL WHERE id = ?", (scan["scanId"],)
)

assert get_scan(state_dir, scan["scanId"])["scan"]["coverage"] == {
"mode": "repository",
"includePaths": ["."],
"excludePaths": [],
"explicitExclusions": [],
}
assert {path: path.read_bytes() for path in scan_dir.rglob("*") if path.is_file()} == artifacts
with sqlite3.connect(state_dir / "workbench.sqlite3") as connection:
assert connection.execute(
"SELECT seal_manifest_digest FROM scans WHERE id = ?", (scan["scanId"],)
).fetchone() == (None,)


@pytest.mark.parametrize(
"corruption",
["coverage", "findings", "missing-coverage", "missing-manifest", "unsealed", "owner", "mode"],
)
def test_get_scan_rejects_unverifiable_legacy_sealed_history_coverage(
tmp_path: Path, corruption: str
) -> None:
state_dir = tmp_path / "state"
repository = tmp_path / "repository"
repository.mkdir()
scan = create_cli_scan(state_dir, tmp_path / "results", repository, finding=False)
scan_dir = Path(scan["scanDir"])
with sqlite3.connect(state_dir / "workbench.sqlite3") as connection:
connection.execute(
"UPDATE scans SET seal_manifest_digest = NULL WHERE id = ?", (scan["scanId"],)
)
manifest_path = scan_dir / "scan-manifest.json"
manifest = json.loads(manifest_path.read_text())
if corruption in {"coverage", "findings"}:
path = scan_dir / f"{corruption}.json"
path.write_bytes(path.read_bytes() + b"\n")
elif corruption.startswith("missing-"):
(
scan_dir
/ ("coverage.json" if corruption == "missing-coverage" else "scan-manifest.json")
).unlink()
elif corruption == "unsealed":
del manifest["scan"]["sealedAt"]
del manifest["scan"]["artifacts"]
manifest_path.write_text(json.dumps(manifest))
elif corruption == "owner":
manifest["scan"]["target"]["targetId"] = "unrelated-target"
manifest_path.write_text(json.dumps(manifest))
else:
coverage_path = scan_dir / "coverage.json"
coverage = json.loads(coverage_path.read_text())
coverage["mode"] = "deep_repository"
coverage_path.write_text(json.dumps(coverage))
for artifact in manifest["scan"]["artifacts"]:
if artifact["path"] == "coverage.json":
artifact["sha256"] = hashlib.sha256(coverage_path.read_bytes()).hexdigest()
manifest_path.write_text(json.dumps(manifest))
artifacts = {path: path.read_bytes() for path in scan_dir.rglob("*") if path.is_file()}

assert "coverage" not in get_scan(state_dir, scan["scanId"])["scan"]
assert {path: path.read_bytes() for path in scan_dir.rglob("*") if path.is_file()} == artifacts
with sqlite3.connect(state_dir / "workbench.sqlite3") as connection:
assert connection.execute(
"SELECT seal_manifest_digest FROM scans WHERE id = ?", (scan["scanId"],)
).fetchone() == (None,)


def test_cli_scan_lifecycle_persists_recipes_lineage_and_filtered_history(tmp_path: Path) -> None:
state_dir = tmp_path / "state"
repository = tmp_path / "repository"
Expand Down
4 changes: 4 additions & 0 deletions sdk/typescript/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,10 @@ directory must be private to you (`chmod 700`).
| `threatModel` / `threatModelPath` | Saved threat model and document path; either can be `null`. |
| `cost` | Estimated model usage cost, or `null` when unavailable. |

Review the requested paths and exclusions alongside the execution outcome.
`coverage.deferred` records unfinished requested work, while
`coverage.openQuestions` records optional follow-up separately.

Review coverage alongside findings. A report with no findings does not establish
that the repository is secure. Reports and logs can contain source code,
vulnerability details, and credentials; review them before sharing.
Expand Down
5 changes: 5 additions & 0 deletions sdk/typescript/docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,11 @@ codex-security scan . --working-tree
codex-security scan . --output-dir /path/outside/repository/results --dry-run
```

`--path` selects files or directories. The scan summary and saved scan details
show requested paths and exclusions. `coverage.deferred` records unfinished
requested work separately from optional `coverage.openQuestions` follow-up.
Human-readable output quotes ambiguous paths, while JSON retains their original strings.

`--diff` scans committed changes; `--working-tree` scans staged and unstaged
changes. Deep scans support whole repositories and path scopes. Working-tree
snapshots include untracked nested Git repositories. Initialized submodules must
Expand Down
1 change: 1 addition & 0 deletions sdk/typescript/scripts/package-dist-files.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const distModules = [
"contract",
"cost",
"cost-model",
"coverage-presentation",
"custom-validation",
"custom-validation-prompt",
"custom-publish",
Expand Down
2 changes: 2 additions & 0 deletions sdk/typescript/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import { promisify, stripVTControlCharacters } from "node:util";
import { Cli, z } from "incur";
import Ajv2020, { type ValidateFunction } from "ajv/dist/2020.js";
import { formatCliHelp } from "./cli-help.js";
import { formatCoverageScope } from "./coverage-presentation.js";
import { scanLogsJson } from "./cli-scan-logs-json.js";
import {
ARTIFACT_EXPORT_FILENAMES,
Expand Down Expand Up @@ -9511,6 +9512,7 @@ function printScanSummary(
? ""
: ` ${paint("THREAT MODEL", "1;36")} ${paint(errorMessage(threatModelPath), 4)}\n`) +
`\n ${paint("FINDINGS", 1)} ${paint(`${findingCount}${findingSummary === "" ? "" : ` (${findingSummary})`}`, findingColor)}\n` +
` ${paint("SCOPE", 1)} ${formatCoverageScope(result.coverage)}\n` +
(deepScanStop === undefined
? ""
: ` ${paint("STOPPED", 1)} ${deepScanStop.reason}\n`) +
Expand Down
Loading
Loading