mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-19 04:59:28 +00:00
129 lines
5.2 KiB
Python
129 lines
5.2 KiB
Python
"""The LLM proposes; the model disposes (model-grounded-detection, Req 8).
|
|||
|
|
|
||
|
|
This module is where the prior architecture's premise is inverted. There, the LLM was an oracle whose noise
|
||
|
|
had to be averaged over K runs, floored per family, and gated by a sign test. Here it answers one bounded,
|
||
|
|
typed question about a candidate it was *handed*, and its answer is checked against the CPG. Three outcomes,
|
||
|
|
and they are exhaustive:
|
||
|
|
|
||
|
|
the model entails it report `model_entailed`, and never call the model at all
|
||
|
|
the model contradicts it drop the claim, recording the contradiction
|
||
|
|
the model cannot decide report `suspicion`, honestly
|
||
|
|
|
||
|
|
Nothing here averages, votes, retries for agreement, or maintains a budget: a deterministic arbiter removes
|
||
|
|
the need for every one of those. The LLM is consulted only for the residual the graph cannot settle — whether
|
||
|
|
a sanitizer on a real flow is actually sufficient — and even that answer only ever *confirms* a flow the CPG
|
||
|
|
already found. It can never conjure one.
|
||
|
|
"""
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import json
|
||
|
|
import logging
|
||
|
|
from collections.abc import Mapping
|
||
|
|
from typing import Any
|
||
|
|
|
||
|
|
from ..cpg.backend import CpgResult
|
||
|
|
from ..redaction import redact_secrets
|
||
|
|
from .ladder import Rung, Verdict
|
||
|
|
from .specs import TaintSpec
|
||
|
|
from .taint import verdict as taint_verdict
|
||
|
|
|
||
|
|
logger = logging.getLogger(__name__)
|
||
|
|
|
||
|
|
QUESTION = """You are judging ONE candidate a static model already found. Do not look for other issues.
|
||
|
|
|
||
|
|
A dataflow analysis resolved this path in {language} code:
|
||
|
|
|
||
|
|
source: {source}
|
||
|
|
sink: {sink} (line {line}, function {function})
|
||
|
|
a sanitizer appears on the path: {sanitized}
|
||
|
|
|
||
|
|
The graph has established that the value reaches the sink. The only open question is whether the sanitizer on
|
||
|
|
that path actually makes it safe for the {family} weakness class.
|
||
|
|
|
||
|
|
Answer ONLY with JSON: {{"vulnerable": true|false, "why": "<one sentence>"}}"""
|
||
|
|
|
||
|
|
|
||
|
|
def judge(
|
||
|
|
cpg: CpgResult,
|
||
|
|
spec: TaintSpec,
|
||
|
|
*,
|
||
|
|
function: str = "",
|
||
|
|
client: Any | None = None,
|
||
|
|
model: str = "",
|
||
|
|
parameter_sources: bool = False,
|
||
|
|
) -> Verdict:
|
||
|
|
"""Arbitrate one candidate. Always returns a verdict; ``suspicion`` is a real answer, not a failure."""
|
||
|
|
answer = taint_verdict(cpg, spec, function=function, parameter_sources=parameter_sources)
|
||
|
|
|
||
|
|
if answer is not None and answer.rung is Rung.ENTAILED:
|
||
|
|
return answer # the graph decided; the model is not consulted
|
||
|
|
|
||
|
|
if answer is None:
|
||
|
|
# No resolved flow. Whatever anyone claims about this site, the model saw nothing to support it.
|
||
|
|
return Verdict(
|
||
|
|
rung=Rung.SUSPICION,
|
||
|
|
family=spec.family,
|
||
|
|
contradiction="the model resolved no flow: no source reaches this sink",
|
||
|
|
)
|
||
|
|
|
||
|
|
# A real flow through a sanitizer. Whether that sanitizer suffices is a semantic judgement the graph
|
||
|
|
# cannot make, so it is the one thing the LLM is asked -- about evidence the model itself produced.
|
||
|
|
if client is None:
|
||
|
|
return Verdict(
|
||
|
|
rung=Rung.SUSPICION,
|
||
|
|
family=spec.family,
|
||
|
|
witness=answer.witness,
|
||
|
|
contradiction="no chat endpoint: the sanitizer's sufficiency was never judged",
|
||
|
|
)
|
||
|
|
|
||
|
|
said_vulnerable = _ask(client, model, spec, answer.witness, function)
|
||
|
|
if said_vulnerable is None:
|
||
|
|
return Verdict(rung=Rung.SUSPICION, family=spec.family, witness=answer.witness,
|
||
|
|
contradiction="the judge returned no usable answer")
|
||
|
|
if not said_vulnerable:
|
||
|
|
return Verdict(rung=Rung.SUSPICION, family=spec.family, witness=answer.witness,
|
||
|
|
contradiction="the judge held the sanitizer on this path sufficient")
|
||
|
|
return answer # CORROBORATED: a real flow the judge agrees is not neutralised
|
||
|
|
|
||
|
|
|
||
|
|
def _ask(client: Any, model: str, spec: TaintSpec, witness: str, function: str) -> bool | None:
|
||
|
|
"""One question, one answer. No tools, no retries for agreement, no second opinion."""
|
||
|
|
source, _, sink = witness.partition(" -> ")
|
||
|
|
line = ""
|
||
|
|
if "(line " in sink:
|
||
|
|
sink, _, tail = sink.partition(" (line ")
|
||
|
|
line = tail.split(",")[0].strip(") ")
|
||
|
|
prompt = redact_secrets(
|
||
|
|
QUESTION.format(
|
||
|
|
language=spec.language, source=source.strip(), sink=sink.strip(), line=line or "?",
|
||
|
|
function=function or "?", sanitized="yes", family=spec.family,
|
||
|
|
)
|
||
|
|
)
|
||
|
|
try:
|
||
|
|
response = client.complete(
|
||
|
|
model=model,
|
||
|
|
messages=[{"role": "user", "content": prompt}],
|
||
|
|
tools=[], # the candidate is given; the model never searches for it
|
||
|
|
json_object=True,
|
||
|
|
)
|
||
|
|
except Exception as exc: # noqa: BLE001 -- an endpoint failure is a degradation, never a verdict
|
||
|
|
logger.warning("judge call failed: %s", exc)
|
||
|
|
return None
|
||
|
|
return _vulnerable(getattr(response, "content", None))
|
||
|
|
|
||
|
|
|
||
|
|
def _vulnerable(content: object) -> bool | None:
|
||
|
|
if not isinstance(content, str) or not content.strip():
|
||
|
|
return None
|
||
|
|
try:
|
||
|
|
payload = json.loads(content)
|
||
|
|
except json.JSONDecodeError:
|
||
|
|
return None
|
||
|
|
if isinstance(payload, Mapping) and isinstance(payload.get("vulnerable"), bool):
|
||
|
|
return bool(payload["vulnerable"])
|
||
|
|
return None
|
||
|
|
|
||
|
|
|
||
|
|
__all__ = ["QUESTION", "judge"]
|