mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-16 19:59:26 +00:00
CI was red on `ruff format --check`: 23 files. I ran `ruff check` after every
change this session and never once ran `ruff format --check`, though CI runs
both. Lint passing is not formatting passing, and I treated one as evidence for
the other for the whole session.
Formatting applied. The full CI sequence now reproduces locally: format, lint,
mypy over 94 files, 735 tests, three gates byte-identical, compileall, and the
semantic job's 16 marked tests.
Also records run 5 of the pipeline measurement, which the vampi fix lifted as
predicted:
hunter (K=5) pipeline (K=1)
pair-correct 76.7% 90.0%
leak 12.3% 6.7%
detection -- 96.7%
cost ~$0.69 $0.0068
27 of 30 pairs. Five runs, each step from a measurement contradicting an
assumption: 73.3 (no pipeline path) -> 73.3 (routing fixed, IDENTICAL, which
exposed the discarded verdict) -> 80.0 (residual question matched to its
arbiter) -> 86.7 (aiohttp settings) -> 90.0 (dominance question no longer
contradicting its own evidence).
One miss left: dsvpwa-dsvpwa-main, whose setting is an argparse --ssl default
rather than a security-setting call. Out of reach before the settings work and
still is; contorting the arbiter around one pair would be worse than the miss.
The caveat is recorded beside the number: 30 pairs of one slice, and the slice
iterated against all session. The +16% overfitting gap against vfc-js is why
contributor-scan Req 8.1/8.2 exist.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01T7XD7x5KonMdmGRkp63EQG
164 lines
7.4 KiB
Python
164 lines
7.4 KiB
Python
"""model-grounded-detection task 3.2: the LLM proposes, the model disposes (Req 8).
|
|
|
|
The judge is where the prior architecture's whole premise is inverted. There is no K-run averaging, no noise
|
|
floor and no acceptance rule; there is one bounded, typed question per candidate, and the answer is CHECKED
|
|
against the CPG rather than voted on. The three outcomes are exhaustive:
|
|
|
|
the model entails it -> report it, and never call the model at all
|
|
the model contradicts it -> drop the claim, and record the contradiction
|
|
the model cannot decide -> report `suspicion`, honestly
|
|
|
|
Every test here runs with no network and a scripted client.
|
|
"""
|
|
|
|
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)
|
|
|
|
|
|
ENTAILING_ROW = [
|
|
{"sink": "os.system(cmd)", "sinkLine": "4", "sinkMethod": "run", "source": "request.args['c']", "sanitized": False, "length": 3}
|
|
]
|
|
|
|
|
|
class _Client:
|
|
"""A scripted chat client that records whether it was called at all."""
|
|
|
|
def __init__(self, answer: str = '{"vulnerable": true, "family": "injection"}') -> None:
|
|
self.calls: list[object] = []
|
|
self._answer = answer
|
|
|
|
def complete(self, **kwargs: object): # type: ignore[no-untyped-def]
|
|
from openultrasast.tool_hunter import ChatResponse
|
|
|
|
self.calls.append(kwargs)
|
|
return ChatResponse(content=self._answer, tool_calls=[], reasoning=None)
|
|
|
|
|
|
def test_an_entailed_candidate_costs_no_model_call() -> None:
|
|
"""The point of the arbiter: where the graph decides, the LLM is not consulted at all."""
|
|
from openultrasast.model.judge import judge
|
|
from openultrasast.model.ladder import Rung
|
|
|
|
client = _Client()
|
|
answer = judge(_cpg(ENTAILING_ROW), _spec(), function="run", client=client, model="m")
|
|
assert answer.rung is Rung.ENTAILED
|
|
assert client.calls == [], "an entailed candidate must not reach the model"
|
|
|
|
|
|
def test_a_claim_the_model_contradicts_is_dropped_with_its_reason() -> None:
|
|
"""Req 8.2. The LLM says vulnerable; the CPG shows no flow reaches the sink. The graph wins."""
|
|
from openultrasast.model.judge import judge
|
|
from openultrasast.model.ladder import Rung
|
|
|
|
client = _Client('{"vulnerable": true, "family": "injection"}')
|
|
answer = judge(_cpg([]), _spec(), function="run", client=client, model="m")
|
|
assert answer.rung is Rung.SUSPICION
|
|
assert answer.contradiction, "a dropped claim must say why it was dropped"
|
|
assert "no flow" in answer.contradiction.lower() or "not reach" in answer.contradiction.lower()
|
|
|
|
|
|
def test_a_claim_the_model_supports_advances_to_corroborated() -> None:
|
|
"""Req 8.3: a sanitized path is a real flow whose sufficiency only the LLM can judge."""
|
|
from openultrasast.model.judge import judge
|
|
from openultrasast.model.ladder import Rung
|
|
|
|
rows = [{"sink": "os.system(cmd)", "sinkLine": "4", "sinkMethod": "run", "source": "request.args['c']", "sanitized": True, "length": 3}]
|
|
client = _Client('{"vulnerable": true, "family": "injection"}')
|
|
answer = judge(_cpg(rows), _spec(), function="run", client=client, model="m")
|
|
assert answer.rung is Rung.CORROBORATED
|
|
assert len(client.calls) == 1, "exactly one bounded question, never a loop"
|
|
|
|
|
|
def test_the_llm_declining_leaves_a_corroborated_flow_at_suspicion() -> None:
|
|
"""The model saw a flow but the LLM says the sanitizer suffices; the claim does not advance."""
|
|
from openultrasast.model.judge import judge
|
|
from openultrasast.model.ladder import Rung
|
|
|
|
rows = [{"sink": "os.system(cmd)", "sinkLine": "4", "sinkMethod": "run", "source": "request.args['c']", "sanitized": True, "length": 3}]
|
|
client = _Client('{"vulnerable": false, "family": "injection"}')
|
|
answer = judge(_cpg(rows), _spec(), function="run", client=client, model="m")
|
|
assert answer.rung is Rung.SUSPICION
|
|
|
|
|
|
def test_without_a_client_the_verdict_still_stands_on_its_own() -> None:
|
|
"""Req 11.3: the endpoint is optional. The model layer does not need it to entail."""
|
|
from openultrasast.model.judge import judge
|
|
from openultrasast.model.ladder import Rung
|
|
|
|
assert judge(_cpg(ENTAILING_ROW), _spec(), function="run", client=None, model="").rung is Rung.ENTAILED
|
|
sanitized = [
|
|
{"sink": "os.system(cmd)", "sinkLine": "4", "sinkMethod": "run", "source": "request.args['c']", "sanitized": True, "length": 3}
|
|
]
|
|
answer = judge(_cpg(sanitized), _spec(), function="run", client=None, model="")
|
|
assert answer.rung is Rung.SUSPICION and "endpoint" in answer.contradiction.lower()
|
|
|
|
|
|
def test_the_question_is_bounded_typed_and_offers_no_tools() -> None:
|
|
"""Req 8.1: the LLM answers one question about a site it was handed; it never searches for the site."""
|
|
from openultrasast.model.judge import judge
|
|
|
|
rows = [{"sink": "os.system(cmd)", "sinkLine": "4", "sinkMethod": "run", "source": "request.args['c']", "sanitized": True, "length": 3}]
|
|
client = _Client()
|
|
judge(_cpg(rows), _spec(), function="run", client=client, model="m")
|
|
call = client.calls[0]
|
|
assert call["tools"] == [], "no tools: the candidate is given, not searched for"
|
|
assert call["json_object"] is True, "a typed answer, not prose"
|
|
text = " ".join(str(m.get("content", "")) for m in call["messages"]) # type: ignore[union-attr]
|
|
assert "os.system(cmd)" in text and "request.args['c']" in text, "the question carries the model's own evidence"
|
|
|
|
|
|
def test_the_prompt_is_redacted() -> None:
|
|
"""Req 11.4: every prompt passes through redaction."""
|
|
from openultrasast.model.judge import judge
|
|
|
|
rows = [
|
|
{
|
|
"sink": "connect(pw)",
|
|
"sinkLine": "4",
|
|
"sinkMethod": "run",
|
|
"source": "AKIA" + "IOSFODNN7" + "EXAMPLE",
|
|
"sanitized": True,
|
|
"length": 2,
|
|
}
|
|
]
|
|
client = _Client()
|
|
judge(_cpg(rows), _spec(), function="run", client=client, model="m")
|
|
text = " ".join(str(m.get("content", "")) for m in client.calls[0]["messages"]) # type: ignore[index,union-attr]
|
|
assert "AKIAIOSFODNN7EXAMPLE" not in text
|
|
|
|
|
|
def test_there_is_no_k_run_averaging_anywhere_in_the_judge() -> None:
|
|
"""Req 8.5, asserted structurally: the noise architecture must not grow back."""
|
|
import ast
|
|
|
|
source = Path("src/openultrasast/model/judge.py").read_text()
|
|
tree = ast.parse(source)
|
|
|
|
# Check CODE, not prose: the docstring is allowed to name the machinery it replaced.
|
|
names = (
|
|
{node.id for node in ast.walk(tree) if isinstance(node, ast.Name)}
|
|
| {node.attr for node in ast.walk(tree) if isinstance(node, ast.Attribute)}
|
|
| {node.name for node in ast.walk(tree) if isinstance(node, ast.FunctionDef)}
|
|
)
|
|
for banned in ("k_runs", "majority", "vote", "votes", "average", "mean", "acceptance", "floor", "budget"):
|
|
assert banned not in names, f"{banned} has no place in the judge"
|
|
|
|
# No loop over runs anywhere in the module.
|
|
assert not [n for n in ast.walk(tree) if isinstance(n, ast.For | ast.While)], "the judge asks once"
|
|
|
|
imported = {n.module for n in ast.walk(tree) if isinstance(n, ast.ImportFrom) and n.module}
|
|
assert not any("rounds" in m or "acceptance" in m for m in imported)
|