mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-16 03:49:26 +00:00
Everything before this built an ARBITER: something that judges a flow taint
reachability had already found unaided. Nothing proposed to it. That is why its
recall was never comparable to a detector's, and why comparing 41.3% entailment
against the LLM hunter's 76.7% pair-correct was comparing two different kinds
of thing. model/judge.py and enumerate_candidates were called by no production
code at all; every number so far came from a measurement harness.
The wiring turns on a distinction judge() alone conflated:
the model CONTRADICTS it has coverage at this site and says no -- a
sanitizer on the path, or a declared-safe sink shape.
The claim is dropped (Req 8.2).
the model CANNOT DECIDE no modelled sink or source reaches the site at all.
The model is SILENT, not negative, so the claim is
reported at `suspicion` (Req 8.4).
Treating silence as refutation is the tempting mistake and would be wrong here:
23 of the 39 misses in the entailment ceiling were sinks simply absent from the
fact tables, so "no flow" usually means the model cannot see it rather than
that nothing is there. The rung carries that uncertainty on the finding's face
instead of a confidence score nobody can act on.
An entailed site still costs zero model calls. The candidate budget is bounded
so a dense region cannot become an unbounded spend.
Smoke-tested end to end against a live endpoint: 2 pairs, detection 100%,
$0.0024. The full vibe-py measurement -- the first like-for-like comparison
with the hunter's 76.7% pair-correct and 12.3% leak -- is running.
727 passing, mypy/ruff clean, zero orphaned modules.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01T7XD7x5KonMdmGRkp63EQG
122 lines
5.6 KiB
Python
122 lines
5.6 KiB
Python
"""Wiring the model layer into a detector (model-grounded-detection, Req 8).
|
|
|
|
Everything before this built an *arbiter* — something that judges a flow taint reachability already found.
|
|
Nothing proposed. This is the piece that makes it a detector: the enumerator offers candidates, the LLM
|
|
answers one bounded question about each, and the model checks the answer.
|
|
|
|
The distinction that matters, and that `judge` alone conflated:
|
|
|
|
the model CONTRADICTS it has coverage here and says no -- a sanitizer on the path, a safe sink shape.
|
|
The claim is dropped (Req 8.2).
|
|
the model CANNOT DECIDE no modelled sink or source reaches this site at all, so the model is silent
|
|
rather than negative. The claim is reported at `suspicion` (Req 8.4).
|
|
|
|
Conflating them is the difference between a precision-first tool that misses most real bugs and an honest one
|
|
that says how sure it is. 23 of 39 misses in the ceiling were absent sinks -- "no flow" usually means the
|
|
model cannot see it, not that nothing is there.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
def _spec(): # type: ignore[no-untyped-def]
|
|
from openultrasast.model.specs import TaintSpec
|
|
|
|
return TaintSpec(family="injection", language="python", sources=("request.args",),
|
|
sinks=("os.system",), sanitizers=("escape",))
|
|
|
|
|
|
def _cpg(rows): # type: ignore[no-untyped-def]
|
|
from openultrasast.cpg.backend import CpgResult
|
|
|
|
return CpgResult(cpg_path=Path("cpg.bin"), run=lambda q, p: rows)
|
|
|
|
|
|
class _Client:
|
|
def __init__(self, answer: str = '{"vulnerable": true, "why": "unsanitised"}') -> None:
|
|
self.calls = 0
|
|
self._answer = answer
|
|
|
|
def complete(self, **kw: object): # type: ignore[no-untyped-def]
|
|
from openultrasast.tool_hunter import ChatResponse
|
|
|
|
self.calls += 1
|
|
return ChatResponse(content=self._answer)
|
|
|
|
|
|
ENTAILING = [{"sink": "os.system(cmd)", "sinkLine": "4", "sinkMethod": "run", "source": "request.args['c']",
|
|
"sanitized": False, "length": 3, "inLabeledScope": True}]
|
|
SANITIZED = [{"sink": "os.system(cmd)", "sinkLine": "4", "sinkMethod": "run", "source": "request.args['c']",
|
|
"sanitized": True, "length": 3, "inLabeledScope": True}]
|
|
|
|
|
|
def test_an_entailed_site_becomes_a_finding_with_no_model_call() -> None:
|
|
from openultrasast.model.ladder import Rung
|
|
from openultrasast.model.pipeline import scan_region
|
|
|
|
client = _Client()
|
|
findings = scan_region(_cpg(ENTAILING), _spec(), function="run", client=client, model="m", candidates=())
|
|
assert len(findings) == 1 and findings[0].rung is Rung.ENTAILED
|
|
assert client.calls == 0
|
|
|
|
|
|
def test_a_sanitized_flow_the_judge_confirms_becomes_corroborated() -> None:
|
|
from openultrasast.model.ladder import Rung
|
|
from openultrasast.model.pipeline import scan_region
|
|
|
|
findings = scan_region(_cpg(SANITIZED), _spec(), function="run", client=_Client(), model="m", candidates=())
|
|
assert len(findings) == 1 and findings[0].rung is Rung.CORROBORATED
|
|
|
|
|
|
def test_a_sanitized_flow_the_judge_rejects_yields_nothing() -> None:
|
|
"""The model found a flow AND has coverage; the judge says the sanitizer suffices. That is a contradiction."""
|
|
from openultrasast.model.pipeline import scan_region
|
|
|
|
client = _Client('{"vulnerable": false, "why": "escaped"}')
|
|
assert scan_region(_cpg(SANITIZED), _spec(), function="run", client=client, model="m", candidates=()) == []
|
|
|
|
|
|
def test_a_candidate_the_model_cannot_see_is_reported_at_suspicion() -> None:
|
|
"""Req 8.4, and the reason recall exists at all: no modelled sink reaches this site, so the model is
|
|
SILENT, not negative. The LLM's claim is reported, honestly labelled."""
|
|
from openultrasast.model.ladder import Rung
|
|
from openultrasast.model.pipeline import scan_region
|
|
|
|
client = _Client()
|
|
findings = scan_region(_cpg([]), _spec(), function="run", client=client, model="m",
|
|
candidates=({"id": "app.py:9:raw_query", "text": "db.raw(q)", "line": 9},))
|
|
assert client.calls == 1, "one bounded question per candidate"
|
|
assert len(findings) == 1 and findings[0].rung is Rung.SUSPICION
|
|
assert findings[0].site == "app.py:9:raw_query"
|
|
|
|
|
|
def test_a_candidate_the_judge_declines_is_not_reported() -> None:
|
|
from openultrasast.model.pipeline import scan_region
|
|
|
|
client = _Client('{"vulnerable": false, "why": "constant"}')
|
|
findings = scan_region(_cpg([]), _spec(), function="run", client=client, model="m",
|
|
candidates=({"id": "app.py:9:x", "text": "db.raw('literal')", "line": 9},))
|
|
assert findings == []
|
|
|
|
|
|
def test_without_a_client_only_what_the_model_establishes_is_reported() -> None:
|
|
"""Req 11.3: the endpoint is optional. Entailment still works; the suspicion band simply goes unasked."""
|
|
from openultrasast.model.ladder import Rung
|
|
from openultrasast.model.pipeline import scan_region
|
|
|
|
assert scan_region(_cpg(ENTAILING), _spec(), function="run", client=None, model="", candidates=())[0].rung is Rung.ENTAILED
|
|
assert scan_region(_cpg([]), _spec(), function="run", client=None, model="",
|
|
candidates=({"id": "a:1:x", "text": "t", "line": 1},)) == []
|
|
|
|
|
|
def test_the_candidate_budget_is_bounded() -> None:
|
|
"""A region with many sites must not turn into an unbounded spend."""
|
|
from openultrasast.model.pipeline import MAX_JUDGED_CANDIDATES, scan_region
|
|
|
|
client = _Client()
|
|
many = tuple({"id": f"a:{i}:x", "text": "db.raw(q)", "line": i} for i in range(50))
|
|
scan_region(_cpg([]), _spec(), function="run", client=client, model="m", candidates=many)
|
|
assert client.calls == MAX_JUDGED_CANDIDATES
|