fix: salvage optional finding fields instead of discarding the finding - #171
fix: salvage optional finding fields instead of discarding the finding#171Haiduongcable wants to merge 5 commits into
Conversation
The unsealed-findings recovery path already salvages malformed writeup, remediationTests, and preventiveControls by dropping just that field and keeping the finding, but any other schema mismatch falls through to the outer handler and discards the entire finding. Two concrete cases hit this without any of the finding's actual content being unrecoverable: - taxonomy.cwe is optional in the hand-rolled validator (defaults to an empty list) but required as a key by the JSON schema, so a finding missing the key outright is silently dropped in full. - codeEvidence is optional at the finding level, but a single malformed entry (e.g. missing `explanation`) currently fails the whole finding instead of just that auxiliary field, unlike its sibling optional fields. This extends the existing salvage pattern to both: codeEvidence joins the existing auxiliary-field recovery list, and a missing taxonomy.cwe is backfilled to an empty list. Dropping codeEvidence can leave dangling evidenceRefs in rootCause/validation/attackPath, so those are pruned in the same pass and a recovery warning is recorded. identity and confidence.rationale are deliberately left as hard requirements in both validators; there is no safe default for identity, and confidence.rationale is a real content requirement rather than a validator mismatch.
The previous change recovered codeEvidence only for schema mismatches the hand-rolled _validate_finding validator doesn't itself check (a missing explanation). Any malformation _validate_finding does check directly -- a non-array codeEvidence, a non-object entry, a missing id, or a duplicate id -- still raised inside _validate_finding before the codeEvidence recovery ran, discarding the whole finding exactly as before. Move codeEvidence validation (schema check plus the same duplicate-id check _validate_finding performs) ahead of _validate_finding, so any malformation is caught and the field dropped -- never the finding -- regardless of which validator would have caught it. Prune dangling evidenceRefs before _validate_finding as well, since that function's own referential check would otherwise fail on the now-removed ids. Also prune the documented nested attackPath.dataflow.evidenceRefs location, not only the top-level attackPath.evidenceRefs sibling field, so a stripped codeEvidence entry cannot leave a dangling reference findings.json still contains no consumer resolves.
|
Fork CI is green on this exact commit, run within the fork so the full matrix executes without Commit:
Local verification (
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e09e0dbdee
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| refs = section.get("evidenceRefs") | ||
| if not isinstance(refs, list): | ||
| continue | ||
| kept_refs = [ref for ref in refs if ref in known_evidence_ids] |
There was a problem hiding this comment.
Reject unhashable evidence references before set lookup
When a rootCause, validation, attackPath, or nested dataflow evidenceRefs array contains a JSON object or array, ref in known_evidence_ids attempts to hash it and raises TypeError. The recovery loop and CLI only catch ContractError, so one malformed optional reference now aborts scan completion with a traceback instead of recovering or discarding that finding as before; validate each reference as a string before the set lookup.
Useful? React with 👍 / 👎.
|
Issue & Root Cause Analysis Problem When there is a little bit of the schema mismatch on the optional fields (taxonomy.cwe not exists or garbage value is there for elements in codeEvidence), the whole findings have been completely dropped in finalizescancontract.py. The New bug noticed during code review When we try to prune orphaned evidenceRefs after cleaning out the ill-formed codeEvidence, we use the lookup on sets ref in knownevidenceids. If one of the elements of a list of evidenceRefs is not hashable (due to ill-formed LLM or scanner's garbage output contains either dictionary/object or a nested list). Then we got TypeError. As we have only wrapped contract checks within ContractError handler, this new type of error will crack the cli application down and leave us with trace backs. Python logic after changes (finalizescancontract.py) In the references pruning logic used for root Cause, validation and attack Path (including in attackPath.dataflow), remove such references: V known_evidence_ids = { def prune_evidence_refs(container: dict) -> bool: |
|
known_evidence_ids = { def prune_evidence_refs(container: dict) -> bool: modified = False if modified: return modified finalize_scan_contract.py |
The dangling-evidenceRefs pruning added alongside codeEvidence recovery tested set membership directly (`ref in known_evidence_ids`) without first checking that each reference was a string. An evidenceRefs entry that is a JSON object or array is unhashable, so the membership test raises TypeError instead of the ContractError the recovery loop and CLI are prepared to handle -- one malformed optional reference now aborted scan completion with a traceback instead of being recovered or discarded like every other malformed field in this path. Guard the membership test with an isinstance check first so non-string entries (objects, arrays, or otherwise-hashable non-string values) are pruned the same way missing or unknown references already are, rather than crashing.
…lds' into fix/finding-recovery-missing-fields
|
Addressed the automated review finding above (P1: unhashable evidence references before set lookup). Root cause: the dangling- Fix: guard the membership test with kept_refs = [
ref for ref in refs if isinstance(ref, str) and ref in known_evidence_ids
]Verification:
|
|
@riju568 thanks for the writeup and the proposed patch — the |
Summary
finalize_scan_contract.pyalready salvages a malformedwriteup,remediationTests, orpreventiveControlsfield by dropping just that field and keeping the finding. Any other schemamismatch falls through to the outer handler and discards the entire finding instead.
Two concrete cases hit this even though the finding's actual content is otherwise fine:
taxonomy.cweis optional in the hand-rolled validator (defaults to an empty list) but requiredas a key by the JSON schema, so a finding missing the key outright is silently dropped in full.
codeEvidenceis optional at the finding level, but a single malformed entry (missingexplanation, wrong shape, missingid, duplicateid, etc.) currently fails the whole findinginstead of just that auxiliary field, unlike its sibling optional fields.
A real, otherwise-valid finding can vanish from
report.md,findings.json, SARIF, and CSV becauseone optional field was malformed, with only a generic completion warning as a trace.
Changes
taxonomy.cwekey to an empty list before final schema validation, matchingthe hand-rolled validator's own tolerant default.
codeEvidence: a malformed entry dropsjust that field, keeping the finding.
codeEvidencerecovery ahead of_validate_finding. The hand-rolled validator independentlyrejects some
codeEvidencemalformations (non-array, non-object entries, missingid, duplicateid) before the schema-based recovery would otherwise run, so validating/strippingcodeEvidencefirst ensures every malformation type is salvaged consistently, not only the ones a bare
JSON-schema check would catch.
codeEvidencecan leave danglingevidenceRefsinrootCause,validation, andattackPath— including the documented nestedattackPath.dataflow.evidenceRefslocation. Theseare pruned in the same pass, with a recovery warning recorded, so the sealed contract never
references removed evidence.
Deliberately not changed
identityandconfidence.rationaleremain hard requirements in both validators. There is no safedefault for
identity, andconfidence.rationaleis a genuine content requirement rather than avalidator mismatch, so a finding missing either is still (correctly) discarded.
Verification
Reproduced directly against
finalize_scan_contract.pyand the bundled example finding before anychange: a missing
taxonomy.cweand five distinct malformed-codeEvidenceshapes each discardedthe whole finding. After the fix, all seven cases are recovered — finding kept, offending
field/reference stripped, warning recorded — while
identity/confidence.rationalecases areunchanged.
Added a regression case to
tests-ts/scan-recovery.test.tscovering all of the above, including thenested
attackPath.dataflow.evidenceRefscase. Confirmed the test fails against unpatchedupstream/mainsource (findingCount4 → 1, all three malformed findings fully discarded) beforepassing against the fix.
Full local gate:
pnpm run types,pnpm run test(702 pass / 5 skip, 0 fail),pnpm run format,pnpm run build, and the packaging check (pnpm pack+check:package) all pass.