Skip to content

Commit 150d6f6

Browse files
authored
fix: recover malformed security scan findings (#86)
1 parent 8edf32b commit 150d6f6

3 files changed

Lines changed: 894 additions & 2 deletions

File tree

sdk/typescript/_bundled_plugin/scripts/finalize_scan_contract.py

Lines changed: 284 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -767,6 +767,267 @@ def _populate_unsealed_finding_identities(
767767
finding["fingerprints"] = fingerprints
768768

769769

770+
def _finding_strength(finding: dict[str, Any]) -> tuple[int, int, int]:
771+
return (
772+
("informational", "low", "medium", "high", "critical").index(finding["severity"]["level"]),
773+
("low", "medium", "high").index(finding["confidence"]["level"]),
774+
len(finding.get("codeEvidence") or []),
775+
)
776+
777+
778+
def _recover_unsealed_findings(
779+
manifest: dict[str, Any],
780+
findings: dict[str, Any],
781+
schema_dir: Path,
782+
scan_dir: Path,
783+
warnings: list[str],
784+
) -> list[str]:
785+
schema = _read_json(schema_dir / "findings.schema.json")
786+
_require_safe_schema(schema, "findings.schema.json")
787+
properties = _require_dict(schema, "properties", "findings.schema")
788+
finding_array = _require_dict(properties, "findings", "findings.schema.properties")
789+
finding_schema = _require_dict(finding_array, "items", "findings.schema.properties.findings")
790+
finding_properties = _require_dict(
791+
finding_schema, "properties", "findings.schema.properties.findings.items"
792+
)
793+
writeup_schema = _require_dict(
794+
finding_properties, "writeup", "findings.schema.properties.findings.items.properties"
795+
)
796+
scan = _require_dict(manifest, "scan", "manifest")
797+
scan_id = _require_str(scan, "id", "manifest.scan")
798+
if findings.get("scanId") != scan_id:
799+
raise ContractError("findings.scanId: must match manifest scan id")
800+
801+
recovered: list[dict[str, Any]] = []
802+
discarded: list[str] = []
803+
finding_positions: dict[str, int] = {}
804+
writeup_paths: set[str] = set()
805+
for index, finding in enumerate(_require_list(findings, "findings", "findings")):
806+
context = f"findings.findings[{index}]"
807+
try:
808+
if not isinstance(finding, dict):
809+
raise ContractError(f"{context}: expected an object")
810+
identity = _require_dict(finding, "identity", context)
811+
fields: list[tuple[dict[str, Any], str, str, str]] = [
812+
(finding, "ruleId", context, "rule identifier"),
813+
(identity, "anchor", f"{context}.identity", "semantic anchor"),
814+
]
815+
if "instance" in identity:
816+
fields.append((identity, "instance", f"{context}.identity", "instance"))
817+
normalized_fields = []
818+
for parent, field, field_context, label in fields:
819+
value = _require_str(parent, field, field_context)
820+
if SLUG_RE.fullmatch(value):
821+
continue
822+
normalized = re.sub(r"[^a-z0-9._/-]+", "-", value.lower()).strip("._/-")
823+
if not SLUG_RE.fullmatch(normalized):
824+
raise ContractError(
825+
f"{field_context}.{field}: expected a stable lowercase semantic slug"
826+
)
827+
parent[field] = normalized
828+
normalized_fields.append(label)
829+
830+
_populate_unsealed_finding_identities(
831+
manifest,
832+
{"scanId": scan_id, "findings": [finding]},
833+
)
834+
finding_id = finding["findingId"]
835+
previous_position = finding_positions.get(finding_id)
836+
_validate_finding(finding, context)
837+
if "writeup" in finding:
838+
try:
839+
_validate_schema_node(finding["writeup"], writeup_schema, f"{context}.writeup")
840+
report_path = finding["writeup"]["reportPath"]
841+
previous_writeup = (
842+
recovered[previous_position].get("writeup")
843+
if previous_position is not None
844+
else None
845+
)
846+
if report_path in writeup_paths and (
847+
previous_writeup is None or previous_writeup["reportPath"] != report_path
848+
):
849+
raise ContractError(f"{context}.writeup.reportPath: duplicate report path")
850+
_require_scan_local_file(scan_dir, report_path, f"{context}.writeup.reportPath")
851+
except ContractError as exc:
852+
finding.pop("writeup")
853+
warnings.append(f"Skipped malformed writeup for finding {index + 1}: {exc}.")
854+
_validate_schema_node(finding, finding_schema, context)
855+
except ContractError as exc:
856+
warning = f"Skipped malformed finding {index + 1}: {exc}."
857+
warnings.append(warning)
858+
discarded.append(warning)
859+
continue
860+
861+
if previous_position is not None:
862+
previous = recovered[previous_position]
863+
if _finding_strength(finding) <= _finding_strength(previous):
864+
warnings.append(
865+
f"Skipped malformed finding {index + 1}: duplicate logical finding."
866+
)
867+
continue
868+
previous_writeup = previous.get("writeup")
869+
if previous_writeup is not None:
870+
writeup_paths.discard(previous_writeup["reportPath"])
871+
recovered[previous_position] = finding
872+
warnings.append(
873+
f"Recovered finding {index + 1}: retained stronger duplicate logical finding."
874+
)
875+
else:
876+
finding_positions[finding_id] = len(recovered)
877+
recovered.append(finding)
878+
879+
if "writeup" in finding:
880+
writeup_paths.add(finding["writeup"]["reportPath"])
881+
if normalized_fields:
882+
warnings.append(
883+
f"Recovered finding {index + 1}: normalized {', '.join(normalized_fields)}."
884+
)
885+
886+
findings["findings"] = recovered
887+
return discarded
888+
889+
890+
def _recover_unsealed_coverage(
891+
coverage: dict[str, Any],
892+
schema_dir: Path,
893+
scan_dir: Path,
894+
warnings: list[str],
895+
discarded_findings: list[str],
896+
) -> None:
897+
schema = _read_json(schema_dir / "coverage.schema.json")
898+
_require_safe_schema(schema, "coverage.schema.json")
899+
properties = _require_dict(schema, "properties", "coverage.schema")
900+
completeness = coverage.get("completeness")
901+
partial = completeness not in ("complete", "partial", "unknown")
902+
if partial:
903+
warnings.append("Recovered malformed coverage completeness; marked coverage as partial.")
904+
905+
surface_ids: set[str] = set()
906+
for field, label in (
907+
("surfaces", "coverage surface"),
908+
("explicitExclusions", "coverage exclusion"),
909+
("deferred", "deferred coverage item"),
910+
):
911+
array_schema = _require_dict(properties, field, "coverage.schema.properties")
912+
item_schema = _require_dict(array_schema, "items", f"coverage.schema.properties.{field}")
913+
items = coverage.get(field)
914+
if not isinstance(items, list):
915+
warnings.append(f"Skipped malformed {label} records: expected an array.")
916+
coverage[field] = []
917+
partial = True
918+
continue
919+
920+
recovered: list[dict[str, Any]] = []
921+
for index, item in enumerate(items):
922+
context = f"coverage.{field}[{index}]"
923+
try:
924+
if not isinstance(item, dict):
925+
raise ContractError(f"{context}: expected an object")
926+
if field == "surfaces":
927+
surface_id = _require_str(item, "id", context)
928+
if surface_id in surface_ids:
929+
raise ContractError(f"{context}.id: duplicate surface id")
930+
disposition = item.get("disposition")
931+
surface_recovered = False
932+
if not isinstance(disposition, str) or disposition not in DISPOSITIONS:
933+
warnings.append(
934+
f"Recovered coverage surface {index + 1}: "
935+
"the review disposition could not be verified."
936+
)
937+
item["disposition"] = "needs_follow_up"
938+
surface_recovered = True
939+
940+
receipt_refs = item.get("receiptRefs")
941+
if not isinstance(receipt_refs, list):
942+
warnings.append(
943+
f"Skipped malformed receipt references for coverage surface "
944+
f"{index + 1}: expected an array."
945+
)
946+
receipt_refs = []
947+
surface_recovered = True
948+
949+
recovered_receipts: list[str] = []
950+
for ref_index, ref in enumerate(receipt_refs):
951+
ref_context = f"{context}.receiptRefs[{ref_index}]"
952+
try:
953+
if not isinstance(ref, str):
954+
raise ContractError(f"{ref_context}: expected a string")
955+
normalized_ref = _require_safe_relative_path(ref, ref_context)
956+
if not normalized_ref.startswith("artifacts/"):
957+
raise ContractError(
958+
f"{ref_context}: expected a file under artifacts/"
959+
)
960+
_require_scan_local_file(scan_dir, normalized_ref, ref_context)
961+
except ContractError as exc:
962+
warnings.append(
963+
f"Skipped malformed coverage receipt "
964+
f"{index + 1}.{ref_index + 1}: {exc}."
965+
)
966+
surface_recovered = True
967+
continue
968+
recovered_receipts.append(normalized_ref)
969+
970+
item["receiptRefs"] = recovered_receipts
971+
if surface_recovered or item["disposition"] == "needs_follow_up":
972+
if not surface_recovered and completeness != "partial":
973+
warnings.append(
974+
f"Coverage surface {index + 1} requires follow-up; "
975+
"marked coverage as partial."
976+
)
977+
item["disposition"] = "needs_follow_up"
978+
partial = True
979+
980+
_validate_schema_node(item, item_schema, context)
981+
except ContractError as exc:
982+
warnings.append(f"Skipped malformed {label} {index + 1}: {exc}.")
983+
partial = True
984+
continue
985+
986+
if field == "surfaces":
987+
surface_ids.add(surface_id)
988+
recovered.append(item)
989+
990+
coverage[field] = recovered
991+
992+
if discarded_findings:
993+
for surface in coverage["surfaces"]:
994+
surface["disposition"] = "needs_follow_up"
995+
coverage["deferred"].extend(
996+
{"id": f"discarded-finding-{index}", "reason": warning}
997+
for index, warning in enumerate(discarded_findings, 1)
998+
)
999+
partial = True
1000+
1001+
if coverage["deferred"] and completeness != "partial":
1002+
if not discarded_findings:
1003+
warnings.append("Coverage has deferred review work; marked coverage as partial.")
1004+
partial = True
1005+
if partial:
1006+
coverage["completeness"] = "partial"
1007+
1008+
1009+
def _recover_unsealed_hardening(
1010+
manifest: dict[str, Any],
1011+
scan_dir: Path,
1012+
warnings: list[str],
1013+
) -> None:
1014+
scan = _require_dict(manifest, "scan", "manifest")
1015+
if "hardening" not in scan:
1016+
return
1017+
1018+
try:
1019+
hardening = _require_dict(scan, "hardening", "manifest.scan")
1020+
portfolio_path = _require_str(hardening, "portfolioPath", "manifest.scan.hardening")
1021+
if portfolio_path != "hardening/hardening.md":
1022+
raise ContractError(
1023+
"manifest.scan.hardening.portfolioPath: expected hardening/hardening.md"
1024+
)
1025+
_require_hardening_portfolio_file(scan_dir, scan)
1026+
except ContractError as exc:
1027+
scan.pop("hardening")
1028+
warnings.append(f"Skipped malformed hardening portfolio: {exc}.")
1029+
1030+
7701031
def _validate_derived_finding_identities(
7711032
manifest: dict[str, Any],
7721033
findings: dict[str, Any],
@@ -1792,8 +2053,21 @@ def build_sarif_projection(
17922053
source_root_is_directory = False
17932054
if not source_root_is_directory:
17942055
raise ContractError("source root: expected an existing directory")
1795-
manifest, findings, _, _ = _read_sealed_scan(scan_dir, schema_dir, "SARIF projection")
2056+
manifest, findings, coverage, _ = _read_sealed_scan(scan_dir, schema_dir, "SARIF projection")
17962057
sarif = build_sarif(manifest, findings, source_root)
2058+
if coverage["completeness"] != "complete":
2059+
run = sarif["runs"][0]
2060+
run["properties"]["codexSecurityCoverageCompleteness"] = coverage["completeness"]
2061+
if coverage["deferred"]:
2062+
run["invocations"] = [
2063+
{
2064+
"executionSuccessful": True,
2065+
"toolExecutionNotifications": [
2066+
{"level": "warning", "message": {"text": item["reason"]}}
2067+
for item in coverage["deferred"]
2068+
],
2069+
}
2070+
]
17972071
_validate_sarif(sarif)
17982072
return sarif
17992073

@@ -2009,6 +2283,7 @@ def _prepare_scan_finalization(
20092283
*,
20102284
expected_coverage_mode: str | None = None,
20112285
completion_binding: dict[str, Any] | None = None,
2286+
completion_warnings: list[str] | None = None,
20122287
) -> PreparedScanFinalization:
20132288
"""Read, populate, and validate a scan without writing any output files."""
20142289

@@ -2061,8 +2336,15 @@ def _prepare_scan_finalization(
20612336
_validate_completion_binding(manifest, findings, coverage, completion_binding)
20622337
if was_sealed:
20632338
_validate_findings(manifest, findings)
2064-
if was_sealed:
20652339
_validate_derived_finding_identities(manifest, findings)
2340+
elif completion_warnings is not None:
2341+
discarded_findings = _recover_unsealed_findings(
2342+
manifest, findings, schema_dir, scan_dir, completion_warnings
2343+
)
2344+
_recover_unsealed_coverage(
2345+
coverage, schema_dir, scan_dir, completion_warnings, discarded_findings
2346+
)
2347+
_recover_unsealed_hardening(manifest, scan_dir, completion_warnings)
20662348
else:
20672349
_populate_unsealed_finding_identities(manifest, findings)
20682350
_validate_findings(manifest, findings)

sdk/typescript/_bundled_plugin/scripts/workbench_db.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1430,6 +1430,7 @@ def complete_scan_locked(
14301430
scan_dir,
14311431
expected_coverage_mode=expected_coverage_mode(scan),
14321432
completion_binding=completion_binding,
1433+
completion_warnings=warnings,
14331434
)
14341435
warning = scan_target_warning(scan)
14351436
if warning is not None and warning not in warnings:

0 commit comments

Comments
 (0)