Skip to content
Open
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
62 changes: 62 additions & 0 deletions sdk/typescript/_bundled_plugin/scripts/finalize_scan_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -796,6 +796,9 @@ def _recover_unsealed_findings(
writeup_schema = _require_dict(
finding_properties, "writeup", "findings.schema.properties.findings.items.properties"
)
code_evidence_schema = _require_dict(
finding_properties, "codeEvidence", "findings.schema.properties.findings.items.properties"
)
auxiliary_schemas = {
name: _require_dict(
finding_properties, name, "findings.schema.properties.findings.items.properties"
Expand Down Expand Up @@ -842,6 +845,65 @@ def _recover_unsealed_findings(
)
finding_id = finding["findingId"]
previous_position = finding_positions.get(finding_id)

if "codeEvidence" in finding:
try:
_validate_schema_node(
finding["codeEvidence"], code_evidence_schema, f"{context}.codeEvidence"
)
seen_evidence_ids: set[str] = set()
for evidence_index, evidence in enumerate(finding["codeEvidence"]):
evidence_id = evidence["id"]
if evidence_id in seen_evidence_ids:
raise ContractError(
f"{context}.codeEvidence[{evidence_index}].id: "
"duplicate code-evidence id"
)
seen_evidence_ids.add(evidence_id)
except ContractError as exc:
finding.pop("codeEvidence")
warnings.append(
f"Skipped malformed codeEvidence for finding {index + 1}: {exc}."
)

taxonomy = finding.get("taxonomy")
if isinstance(taxonomy, dict) and "cwe" not in taxonomy:
taxonomy["cwe"] = []
warnings.append(
f"Recovered finding {index + 1}: backfilled missing taxonomy.cwe with an empty list."
)

known_evidence_ids = {
evidence["id"]
for evidence in finding.get("codeEvidence") or []
if isinstance(evidence, dict) and isinstance(evidence.get("id"), str)
}
attack_path = finding.get("attackPath")
evidence_ref_sections = [
(finding.get("rootCause"), "rootCause"),
(finding.get("validation"), "validation"),
(attack_path, "attackPath"),
]
if isinstance(attack_path, dict):
evidence_ref_sections.append(
(attack_path.get("dataflow"), "attackPath.dataflow")
)
for section, section_name in evidence_ref_sections:
if not isinstance(section, dict):
continue
refs = section.get("evidenceRefs")
if not isinstance(refs, list):
continue
kept_refs = [
ref for ref in refs if isinstance(ref, str) and ref in known_evidence_ids
]
if len(kept_refs) != len(refs):
section["evidenceRefs"] = kept_refs
warnings.append(
f"Recovered finding {index + 1}: dropped dangling {section_name}.evidenceRefs "
"after codeEvidence recovery."
)

_validate_finding(finding, context)
if "writeup" in finding:
try:
Expand Down
195 changes: 195 additions & 0 deletions sdk/typescript/tests-ts/scan-recovery.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -749,6 +749,201 @@ describe("malformed scan artifact recovery", () => {
}
});

test("keeps findings while recovering missing taxonomy CWE and malformed code evidence", async () => {
const fixture = await startDraftScan();
const path = join(fixture.scanDir, "findings.json");
const document = await readJson<FindingsDocument>(path);
const valid = document.findings[0]!;

const missingCwe = structuredClone(valid);
missingCwe.identity.anchor = "missing-cwe";
const missingCweTaxonomy = (missingCwe as Record<string, unknown>)[
"taxonomy"
] as Partial<{ cwe: string[] }>;
delete missingCweTaxonomy.cwe;

const malformedEvidence = structuredClone(valid);
malformedEvidence.identity.anchor = "malformed-evidence";
(malformedEvidence as Record<string, unknown>)["codeEvidence"] = [
// Missing the required `explanation` field.
{
id: "ev1",
label: "sink",
path: "src/extract.py",
startLine: 1,
code: "x = 1",
},
];
(malformedEvidence as Record<string, unknown>)["rootCause"] = {
summary: "Dangling reference regression check.",
evidenceRefs: ["ev1"],
};

// A codeEvidence malformation caught by the hand-rolled validator itself
// (duplicate ids), not only by the JSON-schema pass, plus a dangling
// reference in the nested `attackPath.dataflow.evidenceRefs` location.
const duplicateEvidence = structuredClone(valid);
duplicateEvidence.identity.anchor = "duplicate-evidence";
(duplicateEvidence as Record<string, unknown>)["codeEvidence"] = [
{
id: "ev1",
label: "a",
path: "src/extract.py",
startLine: 1,
code: "x = 1",
explanation: "first",
},
{
id: "ev1",
label: "b",
path: "src/extract.py",
startLine: 2,
code: "y = 2",
explanation: "second",
},
];
(duplicateEvidence as Record<string, unknown>)["attackPath"] = {
summary: "Nested dangling reference regression check.",
dataflow: {
summary: "source -> sink",
source: "input",
sink: "output",
outcome: "impact",
evidenceRefs: ["ev1"],
},
evidenceRefs: ["ev1"],
};

document.findings.push(missingCwe, malformedEvidence, duplicateEvidence);
await writeJson(path, document);

const completed = await completeScan(fixture);

expect(completed.progress.status).toBe("complete");
expect(completed.findingCount).toBe(4);
expect(completed.warnings).toHaveLength(6);
expect(
completed.warnings.some(
(warning) =>
warning.includes("Recovered finding") &&
warning.includes("backfilled missing taxonomy.cwe"),
),
).toBe(true);
expect(
completed.warnings.filter((warning) =>
warning.startsWith("Skipped malformed codeEvidence for finding"),
),
).toHaveLength(2);
expect(
completed.warnings.some(
(warning) =>
warning.includes("Recovered finding") &&
warning.includes("dropped dangling rootCause.evidenceRefs"),
),
).toBe(true);
expect(
completed.warnings.some(
(warning) =>
warning.includes("Recovered finding") &&
warning.includes("dropped dangling attackPath.evidenceRefs"),
),
).toBe(true);
expect(
completed.warnings.some(
(warning) =>
warning.includes("Recovered finding") &&
warning.includes("dropped dangling attackPath.dataflow.evidenceRefs"),
),
).toBe(true);

const recovered = (await readJson<FindingsDocument>(path)).findings;
const missingCweRecovered = recovered.find(
(finding) => finding?.identity.anchor === "missing-cwe",
);
expect(missingCweRecovered).toBeDefined();
expect(
(missingCweRecovered as Record<string, unknown>)?.["taxonomy"],
).toMatchObject({ cwe: [] });

const malformedEvidenceRecovered = recovered.find(
(finding) => finding?.identity.anchor === "malformed-evidence",
);
expect(malformedEvidenceRecovered).toBeDefined();
expect(malformedEvidenceRecovered).not.toHaveProperty("codeEvidence");
expect(
(malformedEvidenceRecovered as Record<string, unknown>)?.["rootCause"],
).toMatchObject({ evidenceRefs: [] });

const duplicateEvidenceRecovered = recovered.find(
(finding) => finding?.identity.anchor === "duplicate-evidence",
);
expect(duplicateEvidenceRecovered).toBeDefined();
expect(duplicateEvidenceRecovered).not.toHaveProperty("codeEvidence");
expect(
(duplicateEvidenceRecovered as Record<string, unknown>)?.["attackPath"],
).toMatchObject({
evidenceRefs: [],
dataflow: { evidenceRefs: [] },
});
});

test("prunes non-string evidenceRefs entries instead of crashing recovery", async () => {
const fixture = await startDraftScan();
const path = join(fixture.scanDir, "findings.json");
const document = await readJson<FindingsDocument>(path);
const valid = document.findings[0]!;

// codeEvidence is malformed so it gets stripped, and evidenceRefs mixes a
// valid string with unhashable (object/array) and non-string (number)
// garbage that a malfunctioning producer could emit. Recovery must prune
// these rather than crash when testing set membership.
const garbageRefs = structuredClone(valid);
garbageRefs.identity.anchor = "garbage-evidence-refs";
(garbageRefs as Record<string, unknown>)["codeEvidence"] = [
{
id: "ev1",
label: "sink",
path: "src/extract.py",
startLine: 1,
code: "x = 1",
},
];
(garbageRefs as Record<string, unknown>)["rootCause"] = {
summary: "Non-string evidenceRefs regression check.",
evidenceRefs: ["ev1", { nested: "garbage" }, ["nested", "garbage"], 42],
};

document.findings.push(garbageRefs);
await writeJson(path, document);

const completed = await completeScan(fixture);

expect(completed.progress.status).toBe("complete");
expect(completed.findingCount).toBe(2);
expect(
completed.warnings.some((warning) =>
warning.startsWith("Skipped malformed codeEvidence for finding"),
),
).toBe(true);
expect(
completed.warnings.some(
(warning) =>
warning.includes("Recovered finding") &&
warning.includes("dropped dangling rootCause.evidenceRefs"),
),
).toBe(true);

const recovered = (await readJson<FindingsDocument>(path)).findings;
const garbageRefsRecovered = recovered.find(
(finding) => finding?.identity.anchor === "garbage-evidence-refs",
);
expect(garbageRefsRecovered).toBeDefined();
expect(garbageRefsRecovered).not.toHaveProperty("codeEvidence");
expect(
(garbageRefsRecovered as Record<string, unknown>)?.["rootCause"],
).toMatchObject({ evidenceRefs: [] });
});

test("keeps verified coverage receipts and downgrades invalid coverage", async () => {
const fixture = await startDraftScan();
const path = join(fixture.scanDir, "coverage.json");
Expand Down