mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-16 19:59:26 +00:00
feat(verification): add evidence ladder verifier
This commit is contained in:
@@ -13,6 +13,7 @@ from .preprocess import preprocess_repository, write_preprocess_artifact
|
||||
from .rank import rank_targets, write_rankings
|
||||
from .reports import write_markdown_report
|
||||
from .run import create_scan_run
|
||||
from .verification import verify_findings, write_verification_results
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
@@ -66,6 +67,8 @@ def _scan(path: Path, config_path: Path, mode: str) -> int:
|
||||
write_rankings(rankings, run.root / "rank" / "ranking.json")
|
||||
findings = runtime.run_stage("quick_findings", lambda: quick_scan_findings(run.target, targets, rankings))
|
||||
write_findings(findings, run.root / "findings.json")
|
||||
verifications = runtime.run_stage("verify", lambda: verify_findings(findings))
|
||||
write_verification_results(verifications, run.root / "verification.json")
|
||||
runtime.run_stage("report", lambda: write_markdown_report(findings, run.root / "report.md"))
|
||||
runtime.finish(status="succeeded")
|
||||
print(f"scan_id={run.scan_id}")
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from dataclasses import asdict, dataclass
|
||||
from enum import StrEnum
|
||||
from pathlib import Path
|
||||
|
||||
from .findings import StaticFinding
|
||||
|
||||
|
||||
class EvidenceLevel(StrEnum):
|
||||
SUSPICION = "suspicion"
|
||||
STATIC_CORROBORATION = "static_corroboration"
|
||||
CRASH_REPRODUCED = "crash_reproduced"
|
||||
ROOT_CAUSE_EXPLAINED = "root_cause_explained"
|
||||
EXPLOIT_DEMONSTRATED = "exploit_demonstrated"
|
||||
PATCH_VALIDATED = "patch_validated"
|
||||
|
||||
|
||||
class VerificationStatus(StrEnum):
|
||||
ACCEPTED = "accepted"
|
||||
REJECTED = "rejected"
|
||||
NEEDS_EVIDENCE = "needs_evidence"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class VerificationResult:
|
||||
finding_id: str
|
||||
status: VerificationStatus
|
||||
evidence_level: EvidenceLevel
|
||||
verified: bool
|
||||
pro_case: str
|
||||
counter_case: str
|
||||
tie_breaker: str
|
||||
required_next_step: str
|
||||
context_sources: list[str]
|
||||
|
||||
|
||||
def validate_evidence_transition(current: EvidenceLevel | str, target: EvidenceLevel | str) -> EvidenceLevel:
|
||||
current_level = EvidenceLevel(current)
|
||||
target_level = EvidenceLevel(target)
|
||||
if _EVIDENCE_ORDER[target_level] < _EVIDENCE_ORDER[current_level]:
|
||||
raise ValueError(f"cannot move evidence level backward: {current_level} -> {target_level}")
|
||||
if _EVIDENCE_ORDER[target_level] - _EVIDENCE_ORDER[current_level] > 1:
|
||||
raise ValueError(f"cannot skip evidence levels: {current_level} -> {target_level}")
|
||||
return target_level
|
||||
|
||||
|
||||
def is_report_verified(evidence_level: EvidenceLevel | str, status: VerificationStatus | str) -> bool:
|
||||
level = EvidenceLevel(evidence_level)
|
||||
verification_status = VerificationStatus(status)
|
||||
return verification_status == VerificationStatus.ACCEPTED and _EVIDENCE_ORDER[level] >= _EVIDENCE_ORDER[EvidenceLevel.STATIC_CORROBORATION]
|
||||
|
||||
|
||||
def build_independent_verifier_context(finding: StaticFinding) -> dict[str, object]:
|
||||
return {
|
||||
"finding_id": finding.finding_id,
|
||||
"path": finding.path,
|
||||
"line": finding.line,
|
||||
"function_name": finding.function_name,
|
||||
"title": finding.title,
|
||||
"severity": finding.severity,
|
||||
"evidence_level": finding.evidence_level,
|
||||
"reachability_status": finding.reachability_status,
|
||||
"reachability_evidence": finding.reachability_evidence,
|
||||
"reachability_conditions": finding.reachability_conditions,
|
||||
"tags": finding.tags,
|
||||
}
|
||||
|
||||
|
||||
def verify_finding(finding: StaticFinding) -> VerificationResult:
|
||||
level = EvidenceLevel(finding.evidence_level)
|
||||
context = build_independent_verifier_context(finding)
|
||||
context_sources = sorted(context.keys())
|
||||
|
||||
if _EVIDENCE_ORDER[level] < _EVIDENCE_ORDER[EvidenceLevel.STATIC_CORROBORATION]:
|
||||
return VerificationResult(
|
||||
finding_id=finding.finding_id,
|
||||
status=VerificationStatus.NEEDS_EVIDENCE,
|
||||
evidence_level=level,
|
||||
verified=False,
|
||||
pro_case="A candidate issue exists, but it has not reached static corroboration.",
|
||||
counter_case="The current evidence can still be a model or heuristic suspicion.",
|
||||
tie_breaker="Add static analyzer, source, or runtime evidence that corroborates the claim.",
|
||||
required_next_step="Collect static corroboration before reporting as verified.",
|
||||
context_sources=context_sources,
|
||||
)
|
||||
|
||||
if finding.reachability_status == "unknown":
|
||||
return VerificationResult(
|
||||
finding_id=finding.finding_id,
|
||||
status=VerificationStatus.REJECTED,
|
||||
evidence_level=level,
|
||||
verified=False,
|
||||
pro_case="A static pattern matched the source line.",
|
||||
counter_case="No function-level attacker-reachable entry point currently reaches this line.",
|
||||
tie_breaker="Provide call-graph, route, CLI, parser, or dynamic evidence linking attacker input to this function.",
|
||||
required_next_step="Add reachability evidence before prioritizing a fix.",
|
||||
context_sources=context_sources,
|
||||
)
|
||||
|
||||
return VerificationResult(
|
||||
finding_id=finding.finding_id,
|
||||
status=VerificationStatus.ACCEPTED,
|
||||
evidence_level=level,
|
||||
verified=is_report_verified(level, VerificationStatus.ACCEPTED),
|
||||
pro_case="Static corroboration exists and function-level reachability evidence is attached.",
|
||||
counter_case="Impact and exploitability still require deeper verifier or dynamic evidence.",
|
||||
tie_breaker="Confirm attacker control, sanitizer absence, and impact in the next verification depth.",
|
||||
required_next_step="Proceed to standard verifier or dynamic reproduction if severity warrants it.",
|
||||
context_sources=context_sources,
|
||||
)
|
||||
|
||||
|
||||
def verify_findings(findings: list[StaticFinding]) -> list[VerificationResult]:
|
||||
return [verify_finding(finding) for finding in findings]
|
||||
|
||||
|
||||
def write_verification_results(results: list[VerificationResult], path: Path) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(json.dumps({"verifications": [asdict(result) for result in results]}, indent=2, sort_keys=True) + "\n")
|
||||
|
||||
|
||||
_EVIDENCE_ORDER = {
|
||||
EvidenceLevel.SUSPICION: 0,
|
||||
EvidenceLevel.STATIC_CORROBORATION: 1,
|
||||
EvidenceLevel.CRASH_REPRODUCED: 2,
|
||||
EvidenceLevel.ROOT_CAUSE_EXPLAINED: 3,
|
||||
EvidenceLevel.EXPLOIT_DEMONSTRATED: 4,
|
||||
EvidenceLevel.PATCH_VALIDATED: 5,
|
||||
}
|
||||
Reference in New Issue
Block a user