Files
Open_Ultra_SAST/src/openultrasast/model/ladder.py
T

75 lines
3.0 KiB
Python
Raw Normal View History

"""The evidence ladder: what established a finding (model-grounded-detection Req 5).
Every finding carries the rung that established it, so a deterministic model result is never confused with an
LLM suspicion. The two middle rungs are the point of this feature:
suspicion a site was enumerated and something flagged it
model_corroborated the LLM's claim is consistent with the CPG -- the source it names reaches the sink it
names, or a guard its siblings carry is absent
model_entailed the CPG establishes it alone, independent of trusting the LLM
execution_confirmed a sandbox reproduced it (deferred tier, memory/C first)
`model_corroborated` and above are established with no model call, and deterministically: run the checker
twice over the same CPG and the verdict sequence is identical. That is what removes the run-to-run noise the
prior architecture tried to average away.
"""
from __future__ import annotations
from dataclasses import dataclass
from enum import StrEnum
from typing import TYPE_CHECKING, TypeVar
if TYPE_CHECKING:
from ..findings import StaticFinding
_Finding = TypeVar("_Finding", bound="StaticFinding")
class Rung(StrEnum):
SUSPICION = "suspicion"
CORROBORATED = "model_corroborated"
ENTAILED = "model_entailed"
EXECUTION_CONFIRMED = "execution_confirmed"
# Ascending strength; `_ORDER.index` is the comparison, so a new rung cannot be silently mis-ranked.
_ORDER = (Rung.SUSPICION, Rung.CORROBORATED, Rung.ENTAILED, Rung.EXECUTION_CONFIRMED)
def at_or_above(rung: Rung, threshold: Rung) -> bool:
return _ORDER.index(rung) >= _ORDER.index(threshold)
@dataclass(frozen=True)
class Verdict:
"""What the model established, and the evidence it established it from."""
rung: Rung
family: str
witness: str = "" # the taint path or the dominance fact; empty only at `suspicion`
contradiction: str = "" # why an LLM claim was dropped, when it was
# ``path:line`` of the evidence, when the arbiter knows it. Once a flow may cross into another module,
# the region that asked the question is no longer where the answer lives, and a finding reported against
# the handler's file when the bug is two modules away is unactionable.
location: str = ""
def at_rung(finding: _Finding, verdict: Verdict | None) -> _Finding:
"""Return ``finding`` carrying ``verdict``'s rung and witness, or unchanged when there is no verdict.
This is the only way a finding rises above ``suspicion`` (Req 5.4). The witness travels with the rung
because a rung without the evidence that justifies it is just a louder assertion.
"""
from dataclasses import replace
if verdict is None or verdict.rung is Rung.SUSPICION:
return finding
rationale = finding.rationale
if verdict.witness and verdict.witness not in rationale:
rationale = f"{rationale} [{verdict.rung.value}: {verdict.witness}]".strip()
return replace(finding, rung=verdict.rung.value, rationale=rationale)
__all__ = ["Rung", "Verdict", "at_or_above", "at_rung"]