106106 capability_preflight_input ,
107107 capability_preflight_json ,
108108 optional_text ,
109+ parse_scan_cost ,
109110 require_occurrence ,
110111 require_uuid ,
111112)
@@ -774,13 +775,32 @@ def require_workspace(connection: sqlite3.Connection, workspace_id: str) -> sqli
774775
775776
776777def require_scan (connection : sqlite3 .Connection , scan_id : str ) -> sqlite3 .Row :
777- scan_id = require_uuid ( scan_id , "scan-id" )
778+ scan_id = resolve_scan_id ( connection , scan_id )
778779 row = connection .execute ("SELECT * FROM scans WHERE id = ?" , (scan_id ,)).fetchone ()
779780 if row is None :
780781 raise SystemExit ("Codex Security scan not found." )
781782 return row
782783
783784
785+ def resolve_scan_id (connection : sqlite3 .Connection , scan_id : str ) -> str :
786+ try :
787+ return str (uuid .UUID (scan_id ))
788+ except ValueError :
789+ if len (scan_id ) < 8 :
790+ raise SystemExit ("Scan ID prefixes must be at least eight characters." ) from None
791+ matches = connection .execute (
792+ "SELECT id FROM scans WHERE substr(id, 1, ?) = ? LIMIT 2" ,
793+ (len (scan_id ), scan_id .lower ()),
794+ ).fetchall ()
795+ if not matches :
796+ raise SystemExit ("Codex Security scan not found." ) from None
797+ if len (matches ) > 1 :
798+ raise SystemExit (
799+ f'Scan ID prefix "{ scan_id } " matches multiple scans; use a longer prefix.'
800+ ) from None
801+ return matches [0 ]["id" ]
802+
803+
784804def create_workspace (connection : sqlite3 .Connection , args : argparse .Namespace ) -> dict [str , Any ]:
785805 workspace_id = require_uuid (args .workspace_id , "workspace-id" )
786806 timestamp = now ()
@@ -1434,12 +1454,16 @@ def pin_legacy_manifest_digest(
14341454
14351455def complete_scan (connection : sqlite3 .Connection , args : argparse .Namespace ) -> dict [str , Any ]:
14361456 scan_id = require_uuid (args .scan_id , "scan-id" )
1457+ cost_json = parse_scan_cost (args .cost_json )
14371458 with scan_completion_lock (scan_id ):
1438- return complete_scan_locked (connection , scan_id , args .claim_token )
1459+ return complete_scan_locked (connection , scan_id , args .claim_token , cost_json )
14391460
14401461
14411462def complete_scan_locked (
1442- connection : sqlite3 .Connection , scan_id : str , claim_token : str | None
1463+ connection : sqlite3 .Connection ,
1464+ scan_id : str ,
1465+ claim_token : str | None ,
1466+ cost_json : str | None ,
14431467) -> dict [str , Any ]:
14441468 scan = require_scan (connection , scan_id )
14451469 if scan ["status" ] == "complete" :
@@ -1532,10 +1556,10 @@ def complete_scan_locked(
15321556 """
15331557 UPDATE scans
15341558 SET status = 'complete', phase = 'reporting', completed_at = ?, updated_at = ?,
1535- seal_manifest_digest = ?
1559+ seal_manifest_digest = ?, cost_json = ?
15361560 WHERE id = ? AND status = 'running'
15371561 """ ,
1538- (timestamp , timestamp , manifest_digest , scan ["id" ]),
1562+ (timestamp , timestamp , manifest_digest , cost_json , scan ["id" ]),
15391563 )
15401564 if updated .rowcount != 1 :
15411565 raise SystemExit ("Only a running scan can be completed." )
@@ -1731,6 +1755,7 @@ def coverage_for_comparison(scan: sqlite3.Row) -> dict[str, Any]:
17311755
17321756def fail_scan (connection : sqlite3 .Connection , args : argparse .Namespace ) -> dict [str , Any ]:
17331757 scan_id = require_uuid (args .scan_id , "scan-id" )
1758+ cost_json = parse_scan_cost (args .cost_json )
17341759 connection .execute ("BEGIN IMMEDIATE" )
17351760 try :
17361761 timestamp = now ()
@@ -1748,10 +1773,17 @@ def fail_scan(connection: sqlite3.Connection, args: argparse.Namespace) -> dict[
17481773 updated = connection .execute (
17491774 """
17501775 UPDATE scans
1751- SET status = 'failed', failure_message = ?, completed_at = ?, updated_at = ?
1776+ SET status = 'failed', failure_message = ?, completed_at = ?, updated_at = ?,
1777+ cost_json = ?
17521778 WHERE id = ? AND status = 'running'
17531779 """ ,
1754- (optional_text (args .message , maximum = 2400 ), timestamp , timestamp , scan ["id" ]),
1780+ (
1781+ optional_text (args .message , maximum = 2400 ),
1782+ timestamp ,
1783+ timestamp ,
1784+ cost_json ,
1785+ scan ["id" ],
1786+ ),
17551787 )
17561788 if updated .rowcount != 1 :
17571789 raise SystemExit ("Only a running scan can be marked failed." )
@@ -3006,6 +3038,11 @@ def scan_result(
30063038 return {
30073039 "artifacts" : artifacts ,
30083040 "canceledAt" : scan ["canceled_at" ],
3041+ ** (
3042+ {"cost" : json .loads (scan ["cost_json" ], parse_constant = reject_non_finite_json )}
3043+ if scan ["cost_json" ] is not None
3044+ else {}
3045+ ),
30093046 "contract" : scan_contract (scan ),
30103047 "continuationThreadId" : scan ["continuation_thread_id" ],
30113048 "failureMessage" : scan ["failure_message" ],
@@ -3208,9 +3245,13 @@ def finding_result(
32083245 "title" : bounded_output_text (occurrence ["title" ], FINDING_TITLE_BYTES ),
32093246 "triage" : finding_triage_result (connection , occurrence ["id" ]),
32103247 }
3211- matches = scan_history .finding_matches (connection , occurrence ["id" ])
3248+ matches , known_since , known_scan_ids = scan_history .finding_matches (
3249+ connection , occurrence ["id" ], scan ["id" ], scan ["started_at" ]
3250+ )
32123251 if matches :
32133252 result ["matches" ] = matches
3253+ result ["knownSince" ] = known_since
3254+ result ["knownScanIds" ] = known_scan_ids
32143255 result .pop ("artifactPaths" , None )
32153256 source_excerpt = finding_source_excerpt (scan , target , locations )
32163257 if source_excerpt :
0 commit comments