mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-16 03:49:26 +00:00
feat(three-stage-scan): overlay complexity ledger without hiding sev-5
This commit is contained in:
@@ -0,0 +1,260 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from openultrasast.complexity.ledger import (
|
||||
NOT_TRIGGERABLE_DELTA,
|
||||
TRIGGERABLE_DELTA,
|
||||
LedgerEntry,
|
||||
apply_overlay,
|
||||
hotspot_key,
|
||||
load_ledger,
|
||||
must_keep_as_candidate,
|
||||
record_verdict,
|
||||
select_forced_candidates,
|
||||
write_ledger,
|
||||
)
|
||||
from openultrasast.complexity.map import Hotspot, build_complexity_map
|
||||
from openultrasast.findings import StaticFinding
|
||||
from openultrasast.policy import CwePolicy
|
||||
from openultrasast.preprocess import FileTarget
|
||||
|
||||
|
||||
def _target(
|
||||
path: str,
|
||||
*,
|
||||
loc: int,
|
||||
tags: list[str] | None = None,
|
||||
language: str = "python",
|
||||
hints: list[dict[str, object]] | None = None,
|
||||
has_fuzz_entry_point: bool = False,
|
||||
) -> FileTarget:
|
||||
return FileTarget(
|
||||
path=path,
|
||||
absolute_path=f"/repo/{path}",
|
||||
language=language,
|
||||
loc=loc,
|
||||
tags=list(tags or []),
|
||||
has_fuzz_entry_point=has_fuzz_entry_point,
|
||||
reachability_hints=list(hints or []),
|
||||
)
|
||||
|
||||
|
||||
def _hit(
|
||||
path: str,
|
||||
finding_id: str,
|
||||
*,
|
||||
function_name: str | None = None,
|
||||
reachability: str = "unknown",
|
||||
severity: str = "low",
|
||||
rationale: str = "inventory",
|
||||
) -> StaticFinding:
|
||||
return StaticFinding(
|
||||
finding_id=finding_id,
|
||||
path=path,
|
||||
title="pattern hit",
|
||||
severity=severity,
|
||||
confidence="medium",
|
||||
evidence_level="static_corroboration",
|
||||
rationale=rationale,
|
||||
line=1,
|
||||
function_name=function_name,
|
||||
reachability_status=reachability,
|
||||
reachability_evidence=[],
|
||||
reachability_conditions=[],
|
||||
tags=[],
|
||||
ranking_priority=1.0,
|
||||
)
|
||||
|
||||
|
||||
def _hotspot(
|
||||
path: str,
|
||||
*,
|
||||
function_name: str | None = None,
|
||||
score: float = 4.0,
|
||||
band: str = "medium",
|
||||
inventory_finding_ids: tuple[str, ...] = (),
|
||||
) -> Hotspot:
|
||||
return Hotspot(
|
||||
path=path,
|
||||
function_name=function_name,
|
||||
score=score,
|
||||
band=band,
|
||||
signals={"loc": 10, "inventory_hit_count": len(inventory_finding_ids)},
|
||||
rationale=f"{band} band: score {score}",
|
||||
test_hint=None,
|
||||
inventory_finding_ids=inventory_finding_ids,
|
||||
)
|
||||
|
||||
|
||||
def test_hotspot_key_joins_path_and_function_or_empty() -> None:
|
||||
assert hotspot_key("parser.c", "parse_header") == "parser.c:parse_header"
|
||||
assert hotspot_key("src/app.py", None) == "src/app.py:"
|
||||
assert hotspot_key("src/app.py", "") == "src/app.py:"
|
||||
|
||||
|
||||
def test_triggerable_overlay_raises_later_score() -> None:
|
||||
hotspot = _hotspot("parser.c", function_name="parse_header", score=4.0, inventory_finding_ids=("hit:1",))
|
||||
ledger = record_verdict({}, path="parser.c", function_name="parse_header", verdict="triggerable")
|
||||
|
||||
overlaid = apply_overlay((hotspot,), ledger)
|
||||
raised = overlaid[0]
|
||||
|
||||
assert ledger[hotspot_key("parser.c", "parse_header")].score_delta == TRIGGERABLE_DELTA
|
||||
assert TRIGGERABLE_DELTA > 0
|
||||
assert raised.score == hotspot.score + TRIGGERABLE_DELTA
|
||||
assert raised.score > hotspot.score
|
||||
assert raised.inventory_finding_ids == ("hit:1",)
|
||||
|
||||
|
||||
def test_not_triggerable_overlay_drops_score_and_keeps_inventory_hit() -> None:
|
||||
hotspot = _hotspot("src/auth.py", function_name="login", score=5.0, inventory_finding_ids=("cmd:src/auth.py:12",))
|
||||
ledger = {
|
||||
hotspot_key("src/auth.py", "login"): LedgerEntry(
|
||||
score_delta=NOT_TRIGGERABLE_DELTA,
|
||||
last_verdict="not_triggerable",
|
||||
round=1,
|
||||
)
|
||||
}
|
||||
|
||||
overlaid = apply_overlay((hotspot,), ledger)
|
||||
demoted = overlaid[0]
|
||||
|
||||
assert NOT_TRIGGERABLE_DELTA < 0
|
||||
assert demoted.score == hotspot.score + NOT_TRIGGERABLE_DELTA
|
||||
assert demoted.score < hotspot.score
|
||||
assert demoted.inventory_finding_ids == ("cmd:src/auth.py:12",)
|
||||
assert demoted.path == hotspot.path
|
||||
assert demoted.function_name == hotspot.function_name
|
||||
|
||||
|
||||
def test_not_triggerable_overlay_drops_map_score_and_sev5_reachable_stays_candidate(tmp_path: Path) -> None:
|
||||
auth = _target(
|
||||
"src/auth.py",
|
||||
loc=80,
|
||||
tags=["auth_boundary"],
|
||||
hints=[{"kind": "route", "access_level": "public", "function_name": "login"}],
|
||||
)
|
||||
helper = _target("src/helper.py", loc=12, tags=[])
|
||||
sev5 = _hit(
|
||||
"src/auth.py",
|
||||
"cmd:src/auth.py:12",
|
||||
function_name="login",
|
||||
reachability="reachable",
|
||||
severity="critical",
|
||||
rationale="Static pattern cmd (CWE-78) matched os.system on line 12; manual verification still required.",
|
||||
)
|
||||
noise = _hit("src/helper.py", "print:src/helper.py:1", reachability="unknown", severity="low")
|
||||
artifact = tmp_path / "complexity_map.json"
|
||||
|
||||
baseline = build_complexity_map([auth, helper], [sev5, noise], artifact)
|
||||
original = next(item for item in baseline.hotspots if item.path == "src/auth.py")
|
||||
assert sev5.finding_id in original.inventory_finding_ids
|
||||
|
||||
ledger_path = tmp_path / ".openultrasast" / "calibration" / "complexity_ledger.json"
|
||||
write_ledger(
|
||||
ledger_path,
|
||||
record_verdict({}, path=original.path, function_name=original.function_name, verdict="not_triggerable"),
|
||||
)
|
||||
|
||||
overlaid = build_complexity_map([auth, helper], [sev5, noise], artifact, ledger_path=ledger_path)
|
||||
demoted = next(item for item in overlaid.hotspots if item.path == "src/auth.py")
|
||||
|
||||
assert demoted.score < original.score
|
||||
assert demoted.score == original.score + NOT_TRIGGERABLE_DELTA
|
||||
assert demoted.inventory_finding_ids == original.inventory_finding_ids
|
||||
assert sev5.finding_id in demoted.inventory_finding_ids
|
||||
assert sev5.severity == "critical"
|
||||
assert sev5.rationale == ("Static pattern cmd (CWE-78) matched os.system on line 12; manual verification still required.")
|
||||
|
||||
policy = {
|
||||
sev5.finding_id: 5,
|
||||
noise.finding_id: 2,
|
||||
}
|
||||
forced = select_forced_candidates([sev5, noise], policy)
|
||||
assert forced == (sev5,)
|
||||
assert must_keep_as_candidate(sev5, 5, "reachable") is True
|
||||
assert must_keep_as_candidate(noise, 2, "unknown") is False
|
||||
|
||||
|
||||
def test_must_keep_as_candidate_requires_sev5_and_reachable_surface() -> None:
|
||||
reachable = _hit("a.py", "a:1", reachability="reachable")
|
||||
inferred = _hit("b.py", "b:1", reachability="inferred-file-surface")
|
||||
unknown = _hit("c.py", "c:1", reachability="unknown")
|
||||
|
||||
assert must_keep_as_candidate(reachable, 5, "reachable") is True
|
||||
assert must_keep_as_candidate(inferred, 5, "inferred-file-surface") is True
|
||||
assert must_keep_as_candidate(unknown, 5, "unknown") is False
|
||||
assert must_keep_as_candidate(reachable, 4, "reachable") is False
|
||||
assert must_keep_as_candidate(reachable, 5, None) is True
|
||||
|
||||
|
||||
def test_select_forced_candidates_resolves_policy_severity() -> None:
|
||||
sev5 = _hit("src/auth.py", "cmd:src/auth.py:12", reachability="reachable")
|
||||
inferred = _hit("src/upload.py", "sql:src/upload.py:4", reachability="inferred-file-surface")
|
||||
mild = _hit("src/helper.py", "print:src/helper.py:1", reachability="reachable")
|
||||
policy = {
|
||||
"CWE-78": CwePolicy("Command Injection", 5, True, False),
|
||||
"CWE-89": CwePolicy("SQL Injection", 5, True, False),
|
||||
"CWE-116": CwePolicy("Encode Output", 2, True, False),
|
||||
}
|
||||
|
||||
forced = select_forced_candidates(
|
||||
[mild, sev5, inferred],
|
||||
policy,
|
||||
rule_cwe={
|
||||
sev5.finding_id: "CWE-78",
|
||||
inferred.finding_id: "CWE-89",
|
||||
mild.finding_id: "CWE-116",
|
||||
},
|
||||
)
|
||||
|
||||
assert forced == (sev5, inferred)
|
||||
|
||||
|
||||
def test_ledger_round_trip_matches_design_schema(tmp_path: Path) -> None:
|
||||
path = tmp_path / "complexity_ledger.json"
|
||||
key = hotspot_key("parser.c", "parse_header")
|
||||
write_ledger(
|
||||
path,
|
||||
{key: LedgerEntry(score_delta=1.5, last_verdict="triggerable", round=3)},
|
||||
)
|
||||
payload = json.loads(path.read_text())
|
||||
|
||||
assert payload == {
|
||||
key: {
|
||||
"last_verdict": "triggerable",
|
||||
"round": 3,
|
||||
"score_delta": 1.5,
|
||||
}
|
||||
}
|
||||
loaded = load_ledger(path)
|
||||
assert loaded[key] == LedgerEntry(score_delta=1.5, last_verdict="triggerable", round=3)
|
||||
assert load_ledger(tmp_path / "missing.json") == {}
|
||||
assert load_ledger(None) == {}
|
||||
|
||||
|
||||
def test_overlay_reorders_and_rebands_without_dropping_hits() -> None:
|
||||
high = _hotspot("src/auth.py", function_name="login", score=3.2, band="medium", inventory_finding_ids=("sev5:1",))
|
||||
other = _hotspot("src/helper.py", score=2.0, band="low", inventory_finding_ids=("low:1",))
|
||||
ledger = record_verdict({}, path="src/auth.py", function_name="login", verdict="not_triggerable")
|
||||
|
||||
overlaid = apply_overlay((high, other), ledger)
|
||||
|
||||
assert [item.path for item in overlaid] == ["src/helper.py", "src/auth.py"]
|
||||
demoted = overlaid[1]
|
||||
assert demoted.score == round(3.2 + NOT_TRIGGERABLE_DELTA, 4)
|
||||
assert demoted.band == "low"
|
||||
assert demoted.inventory_finding_ids == ("sev5:1",)
|
||||
assert overlaid[0].inventory_finding_ids == ("low:1",)
|
||||
|
||||
|
||||
def test_build_complexity_map_without_ledger_path_skips_overlay(tmp_path: Path) -> None:
|
||||
target = _target("src/app.py", loc=20)
|
||||
finding = _hit("src/app.py", "rule:src/app.py:1")
|
||||
artifact = tmp_path / "complexity_map.json"
|
||||
|
||||
first = build_complexity_map([target], [finding], artifact)
|
||||
second = build_complexity_map([target], [finding], artifact)
|
||||
|
||||
assert first.hotspots[0].score == second.hotspots[0].score
|
||||
assert first.hotspots[0].inventory_finding_ids == (finding.finding_id,)
|
||||
Reference in New Issue
Block a user