mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-19 13:10:03 +00:00
feat(model-grounded-detection): tasks 2.1-2.2, the Joern seam and the taint verdict
The whole engine integration is one subprocess boundary: joern-parse builds a cpg.bin, joern --script runs a shipped CPGQL query that prints its JSON between fence markers, because Joern's stdout carries a banner and a REPL prompt around whatever a script emits. Nothing is imported in-process, so the core install keeps dependencies = [] -- a test walks the seam's AST to assert it imports only the standard library and names no JVM binding. Fail closed everywhere. A build or query that errors, times out, or prints something unparseable yields no verdict and the caller reports suspicion. The taint verdict distinguishes this from absence deliberately: a query returning None means "the engine could not decide", which is not "there is no flow", and conflating them is how a missing engine silently becomes a clean bill of health. The verdict is where the ladder earns its keep. An unsanitized source->sink path is model_entailed -- the graph establishes it and the LLM's claim is no longer load-bearing. A path a sanitizer may break is model_corroborated: the flow is real, but whether the sanitizer suffices is the semantic residual the LLM answers, checked against this verdict rather than averaged over runs. The chosen flow is picked by a total order (shortest, then lexicographic), so two runs over one CPG cannot disagree -- Req 6.4 asserted directly. The query itself is the part our flat IR could not do: reachableByFlows walks the PDG, so `q = "..." + request.args["x"]; execute(q)` is found, which is exactly the one-hop limit that held measured entailment at 2.2%. Capability detection reuses semantic/engines.joern_available rather than growing a second notion of "installed". The audit manifest is regenerated; the new modules are named by Req 4/5/6 rather than counted as orphans. Task 2.3, the go/no-go measurement, needs Joern installed and has not run. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01T7XD7x5KonMdmGRkp63EQG
This commit is contained in:
co-authored by
Claude Opus 5
parent
9ed0e45c5f
commit
2b973dc0db
@@ -0,0 +1,73 @@
|
||||
"""Taint reachability over the CPG: the arbiter for the flow families (Req 6.1).
|
||||
|
||||
The verdict is the static analogue of a reproduced crash. An unsanitized source→sink path is `ENTAILED` — the
|
||||
graph establishes it and the LLM's claim is no longer load-bearing. A path a sanitizer may break is
|
||||
`CORROBORATED`: the flow is real, but whether the sanitizer is *sufficient* is a semantic judgement the model
|
||||
cannot make, and that residual is the LLM's — checked against this verdict rather than averaged over runs.
|
||||
|
||||
No path at all yields `None`, not a negative verdict: the caller reports `suspicion`. And a query that failed
|
||||
yields `None` too, because "the engine could not decide" is not "there is no flow" — conflating them is how a
|
||||
missing engine would silently become a clean bill of health.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping, Sequence
|
||||
|
||||
from ..cpg.backend import CpgResult
|
||||
from .ladder import Rung, Verdict
|
||||
from .specs import TaintSpec
|
||||
|
||||
|
||||
def verdict(cpg: CpgResult, spec: TaintSpec, *, function: str = "") -> Verdict | None:
|
||||
"""The strongest verdict the taint query supports for ``spec`` in ``function``, or ``None`` to stay at suspicion."""
|
||||
rows = cpg.run(
|
||||
"taint",
|
||||
{"sources": spec.sources, "sinks": spec.sinks, "sanitizers": spec.sanitizers, "function": function},
|
||||
)
|
||||
flows = _flows(rows, function=function)
|
||||
if not flows:
|
||||
return None
|
||||
unsanitized = [flow for flow in flows if not flow["sanitized"]]
|
||||
# Deterministic by construction: sort, then take the first. Nothing here depends on engine ordering.
|
||||
chosen = min(unsanitized or flows, key=_rank)
|
||||
rung = Rung.ENTAILED if not chosen["sanitized"] else Rung.CORROBORATED
|
||||
return Verdict(rung=rung, family=spec.family, witness=_witness(chosen))
|
||||
|
||||
|
||||
def _flows(rows: object, *, function: str) -> list[dict[str, object]]:
|
||||
"""The query's rows, filtered to the labeled function. A non-list (a failure) yields nothing."""
|
||||
if not isinstance(rows, Sequence) or isinstance(rows, (str, bytes)):
|
||||
return []
|
||||
kept: list[dict[str, object]] = []
|
||||
for row in rows:
|
||||
if not isinstance(row, Mapping):
|
||||
continue
|
||||
if function and str(row.get("sinkMethod", "")) != function:
|
||||
continue
|
||||
kept.append({
|
||||
"sink": str(row.get("sink", "")),
|
||||
"sinkLine": str(row.get("sinkLine", "")),
|
||||
"sinkMethod": str(row.get("sinkMethod", "")),
|
||||
"source": str(row.get("source", "")),
|
||||
"sanitized": bool(row.get("sanitized", False)),
|
||||
"length": _as_int(row.get("length")),
|
||||
})
|
||||
return kept
|
||||
|
||||
|
||||
def _as_int(value: object) -> int:
|
||||
return int(value) if isinstance(value, int | float) else 0
|
||||
|
||||
|
||||
def _rank(flow: Mapping[str, object]) -> tuple[int, str, str]:
|
||||
"""Shortest flow first, then lexicographic — a total order, so two runs cannot disagree."""
|
||||
return (_as_int(flow["length"]), str(flow["source"]), str(flow["sink"]))
|
||||
|
||||
|
||||
def _witness(flow: Mapping[str, object]) -> str:
|
||||
line = flow.get("sinkLine") or "?"
|
||||
return f"{flow['source']} -> {flow['sink']} (line {line}, {flow['length']} steps)"
|
||||
|
||||
|
||||
__all__ = ["verdict"]
|
||||
Reference in New Issue
Block a user