Files
Open_Ultra_SAST/tests/test_model_taint.py
T
Marius CiepluchandClaude Opus 5 1765e51da1 feat(model-grounded-detection): the safe-shape test, without which the model arbitrates nothing
The first live run produced the finding that shapes this design: taint
reachability alone does not distinguish a fix from a bug. On the canonical SQL
pair the flow request.args["name"] -> name -> execute exists on BOTH sides --

  vuln   db.execute("select ... '" + name + "'")   one argument, interpolated
  fixed  db.execute("select ... = %s", (name,))    two arguments, bound

-- so the model entailed the fixed twin exactly as readily as the vulnerable
one, and arbitrated nothing. What separates them is the SHAPE of the sink call,
which is precisely what safe_shape_sinks preserved from the earlier sanitizer
fix. The query now reports sinkArity and sinkArg0Literal, and a flow ending at
a sink written in its declared-safe form is not a vulnerability.

Two bugs in that shape test, both found only against the real engine:
Joern's `call.argument` includes the RECEIVER at argumentIndex 0, so `db` was
counted as an argument -- making a one-argument interpolated call look like a
two-argument bound one, the exact inversion of the test -- and the "first
argument" whose literalness decides a format-string sink was the receiver
rather than the format. Both fixed with argumentIndexGt(0).

The rule is narrow on purpose: only sinks named in safe_shape_sinks are subject
to it (os.system(x) is not safe merely because it takes one argument), and a
row carrying no shape fields is judged on the flow alone -- an unknown shape
must never be read as a safe one.

End to end on the pair, deterministic and with no model call:
  vuln  -> model_entailed  request.args["name"] -> db.execute(...) (line 4)
  fixed -> None

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01T7XD7x5KonMdmGRkp63EQG
2026-09-08 12:51:42 +02:00

185 lines
7.9 KiB
Python

"""model-grounded-detection task 2.2: the taint verdict, and what separates entailed from corroborated.
An `ENTAILED` verdict is the static analogue of a reproduced crash: the CPG establishes the flow on its own and
the LLM's assertion is no longer load-bearing. `CORROBORATED` means a path exists but a sanitizer may break it,
so the judgement of sufficiency is the LLM's residual — checked against the graph, never averaged over runs.
"""
from __future__ import annotations
from pathlib import Path
def _cpg(rows: list[dict[str, object]]): # type: ignore[no-untyped-def]
from openultrasast.cpg.backend import CpgResult
return CpgResult(cpg_path=Path("cpg.bin"), run=lambda query, params: rows)
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", "execute"),
sanitizers=("escape", "quote"),
)
def test_an_unsanitized_source_to_sink_path_is_entailed() -> None:
from openultrasast.model.ladder import Rung
from openultrasast.model.taint import verdict
rows = [
{"sink": "os.system(cmd)", "sinkLine": "4", "sinkMethod": "run", "source": "request.args['x']", "sanitized": False, "length": 3}
]
answer = verdict(_cpg(rows), _spec(), function="run")
assert answer is not None and answer.rung is Rung.ENTAILED
assert answer.family == "injection"
assert "request.args" in answer.witness and "os.system" in answer.witness, "the witness names the flow it found"
def test_a_path_through_a_sanitizer_is_corroborated_not_entailed() -> None:
"""The model saw the flow but cannot judge whether the sanitizer is sufficient. That residual is the LLM's."""
from openultrasast.model.ladder import Rung
from openultrasast.model.taint import verdict
rows = [{"sink": "execute(q)", "sinkLine": "9", "sinkMethod": "run", "source": "request.args['q']", "sanitized": True, "length": 4}]
answer = verdict(_cpg(rows), _spec(), function="run")
assert answer is not None and answer.rung is Rung.CORROBORATED
def test_no_path_yields_no_verdict_so_the_caller_reports_suspicion() -> None:
from openultrasast.model.taint import verdict
assert verdict(_cpg([]), _spec(), function="run") is None
def test_a_query_that_failed_is_not_read_as_an_absence_of_flow() -> None:
"""`None` from the engine means "could not decide", which is not the same as "no path" — Req 4.3."""
from openultrasast.cpg.backend import CpgResult
from openultrasast.model.taint import verdict
broken = CpgResult(cpg_path=Path("cpg.bin"), run=lambda query, params: None)
assert verdict(broken, _spec(), function="run") is None
def test_the_verdict_is_restricted_to_the_labeled_function() -> None:
from openultrasast.model.taint import verdict
rows = [
{
"sink": "os.system(x)", "sinkLine": "40", "sinkMethod": "elsewhere",
"source": "request.args['x']", "sanitized": False, "length": 2,
}
]
assert verdict(_cpg(rows), _spec(), function="run") is None, "a flow in another function does not judge this one"
def test_the_shortest_unsanitized_flow_wins_and_the_sequence_is_deterministic() -> None:
"""Req 6.4: two runs over one CPG give an identical verdict sequence, so there is nothing to average."""
from openultrasast.model.ladder import Rung
from openultrasast.model.taint import verdict
rows = [
{"sink": "os.system(a)", "sinkLine": "7", "sinkMethod": "run", "source": "request.args['b']", "sanitized": True, "length": 9},
{"sink": "os.system(a)", "sinkLine": "7", "sinkMethod": "run", "source": "request.args['a']", "sanitized": False, "length": 2},
]
first = verdict(_cpg(rows), _spec(), function="run")
second = verdict(_cpg(rows), _spec(), function="run")
assert first == second
assert first is not None and first.rung is Rung.ENTAILED and "request.args['a']" in first.witness
def test_the_spec_drives_the_query_parameters() -> None:
"""The security modelling is ours; the traversal is the engine's. The spec is what crosses the seam."""
from openultrasast.cpg.backend import CpgResult
from openultrasast.model.taint import verdict
seen: dict[str, object] = {}
def run(query: str, params: dict[str, object]) -> object:
seen["query"] = query
seen.update(params)
return []
verdict(CpgResult(cpg_path=Path("cpg.bin"), run=run), _spec(), function="run")
assert seen["query"] == "taint"
assert seen["sources"] == ("request.args",)
assert seen["sinks"] == ("os.system", "execute")
assert seen["sanitizers"] == ("escape", "quote")
assert seen["function"] == "run"
# --- the safe-shape test (task 2.3 finding) ------------------------------------------------------------
#
# Taint reachability alone does not distinguish a parameterised fix from an interpolated bug: the flow
# `request.args["name"] -> name -> execute` exists on BOTH sides of the canonical SQL pair.
#
# vuln db.execute("select ... '" + name + "'") one argument, interpolated
# fixed db.execute("select ... = %s", (name,)) two arguments, bound
#
# What separates them is the SHAPE of the sink call, which is exactly what `TaintSpec.safe_shape_sinks`
# preserves. Without this test the model entails both sides and arbitrates nothing.
def _shape_spec(): # type: ignore[no-untyped-def]
from openultrasast.model.specs import TaintSpec
return TaintSpec(
family="injection",
language="python",
sources=("request.args",),
sinks=("execute",),
sanitizers=(),
safe_shape_sinks=("execute",),
)
def test_a_parameterised_sink_call_is_not_a_vulnerability_even_though_the_flow_reaches_it() -> None:
from openultrasast.model.taint import verdict
rows = [{
"sink": 'db.execute("select ... = %s", (name,))', "sinkLine": "5", "sinkMethod": "run",
"source": "request.args['name']", "sanitized": False, "length": 4, "sinkArity": 2, "sinkArg0Literal": True,
}]
assert verdict(_cpg(rows), _shape_spec(), function="run") is None, "a bound query is the fix, not the bug"
def test_an_interpolated_sink_call_of_the_same_name_is_entailed() -> None:
from openultrasast.model.ladder import Rung
from openultrasast.model.taint import verdict
rows = [{
"sink": 'db.execute("select ... \'" + name + "\'")', "sinkLine": "5", "sinkMethod": "run",
"source": "request.args['name']", "sanitized": False, "length": 4, "sinkArity": 1, "sinkArg0Literal": False,
}]
answer = verdict(_cpg(rows), _shape_spec(), function="run")
assert answer is not None and answer.rung is Rung.ENTAILED
def test_the_shape_test_only_applies_to_sinks_declared_safe_in_that_shape() -> None:
"""`os.system(x)` with one argument is not "safe because it has one argument" -- the rule is per sink."""
from openultrasast.model.ladder import Rung
from openultrasast.model.taint import verdict
rows = [{
"sink": "os.system(full)", "sinkLine": "7", "sinkMethod": "run",
"source": "request.args['cmd']", "sanitized": False, "length": 5, "sinkArity": 1, "sinkArg0Literal": False,
}]
answer = verdict(_cpg(rows), _spec(), function="run") # os.system is not in safe_shape_sinks
assert answer is not None and answer.rung is Rung.ENTAILED
def test_a_row_without_shape_fields_is_judged_on_the_flow_alone() -> None:
"""Older rows, or an engine that could not report arity, must not be silently treated as safe."""
from openultrasast.model.ladder import Rung
from openultrasast.model.taint import verdict
rows = [{"sink": "db.execute(q)", "sinkLine": "5", "sinkMethod": "run", "source": "request.args['n']",
"sanitized": False, "length": 3}]
answer = verdict(_cpg(rows), _shape_spec(), function="run")
assert answer is not None and answer.rung is Rung.ENTAILED