feat(model-grounded-detection): 4.2 constant abstraction, the third arbiter form

A configuration bug has no flow to follow and no guard to dominate.
CORS(app, origins="*") is dangerous because of the value it is SET TO, so the
arbiter evaluates the setting's arguments against the closed permissive set in
the obligation facts -- the third form beside taint reachability and guard
dominance, and the one Req 6.3 names.

  ENTAILED      a security setting is passed a literal in the permissive set
  CORROBORATED  the argument is computed, so constant abstraction cannot decide
                it -- that residual is the judge's, never silently "safe"
  (none)        every literal is outside the set, or the call is not a setting

Two Joern node-shape bugs, both found by running it and both of the same class
as group 2's receiver bug -- the graph's indexing is not what the source text
suggests:

  * a keyword argument (`origins="*"`) is an assignment-shaped node, so
    isLiteral is false on the argument itself and the literal sits in its
    subtree; literals are collected from the argument's AST now.
  * Joern indexes the receiver at 0, positional arguments from 1, and NAMED
    arguments at -1. Settings are configured overwhelmingly by keyword, so
    argumentIndexGt(0) -- correct when counting arity for the taint shape test
    -- dropped exactly the arguments that matter and every keyword-configured
    setting read as "computed".

Live, deterministic, zero model calls:
  create_app -> model_entailed  CORS(app, origins = "*"): permissive literal "*"
  safe_app   -> no finding      (origins = "https://app.example")

All three arbiter forms now work against a real CPG: taint reachability for the
flow families, guard dominance for absence, constant abstraction for config.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01T7XD7x5KonMdmGRkp63EQG
This commit is contained in:
Marius Ciepluch
2026-09-08 15:00:41 +02:00
co-authored by Claude Opus 5
parent fec67253f3
commit d9ceeb6a09
6 changed files with 245 additions and 3 deletions
@@ -1,13 +1,13 @@
{
"counts": {
"load_bearing": 81,
"load_bearing": 82,
"standalone_capability": 7
},
"generated": "2026-09-08 (regenerated after group 4 added the dominance arbiter)",
"generated": "2026-09-08 (regenerated after group 4: dominance and constant abstraction)",
"kept_with_a_lever_stripped": {
"improve/evolve.py + improve/validator.py": "the design's Removed list named these whole, but each is a mix: the mechanism lever (removed) and the deterministic rule-status improve loop that `ousast improve` still runs from benchmark evidence, which has no LLM in it and is not what this feature supersedes. Deleting the files would have taken a retained capability with them."
},
"modules": 88,
"modules": 89,
"note": "Classification of every source module after the group-1 subtraction. load_bearing = reachable from a gate, the CLI, or named by a surviving requirement; standalone_capability = its own entry point or protocol, deliberately off the scan path; orphaned = no importer, no entry point, no surviving requirement -- removed, never carried.",
"orphaned": [],
"removed_in_this_pass": {
@@ -128,6 +128,7 @@
"classification": "load_bearing",
"importers": [
"cpg.backend",
"model.config_value",
"model.dominance",
"model.judge",
"model.taint"
@@ -138,6 +139,7 @@
{
"classification": "load_bearing",
"importers": [
"model.config_value",
"model.dominance",
"model.judge",
"model.taint"
@@ -326,6 +328,7 @@
"importers": [
"cli",
"model.candidates",
"model.config_value",
"model.dominance",
"model.judge",
"model.specs",
@@ -348,6 +351,12 @@
"module": "model.candidates",
"reason": "reachable from gate, map_gate, pair_gate or the CLI"
},
{
"classification": "load_bearing",
"importers": [],
"module": "model.config_value",
"reason": "model-grounded-detection Req 6.3: constant abstraction for the configuration families"
},
{
"classification": "load_bearing",
"importers": [],
@@ -369,6 +378,7 @@
{
"classification": "load_bearing",
"importers": [
"model.config_value",
"model.dominance",
"model.judge",
"model.taint"
@@ -379,6 +389,7 @@
{
"classification": "load_bearing",
"importers": [
"model.config_value",
"model.dominance",
"model.judge",
"model.taint"
+44
View File
@@ -0,0 +1,44 @@
// Security settings and the literal values they are given (model-grounded-detection Req 6.3).
//
// A configuration bug has no flow and no guard: `CORS(app, origins="*")` is dangerous because of the value.
// This query reports each setting call with its arguments split into literals (which the model can evaluate)
// and non-literals (which it cannot -- those become `corroborated`, never silently safe).
//
// Parameters: cpgFile, settings, function (optional).
// Output: fenced JSON array of {setting, line, method, args, literalArgs}.
@main def exec(cpgFile: String, settings: String, function: String = "") = {
importCpg(cpgFile)
def split(raw: String): List[String] = raw.split(",").map(_.trim).filter(_.nonEmpty).toList
val names = split(settings)
def matches(name: String): Boolean = names.exists(n => name == n || name == n.split("\\.").last)
val calls = {
val all = cpg.call.filter(c => matches(c.name)).l
if (function.isEmpty) all else all.filter(_.method.name == function)
}
val rows = calls.map { call =>
// Joern indexes the receiver at 0, positional arguments from 1, and NAMED arguments at -1. A setting is
// configured overwhelmingly by keyword (`origins="*"`, `secure=False`), so filtering `argumentIndexGt(0)`
// -- correct when counting arity for the taint shape test -- drops exactly the arguments that matter here
// and every keyword-configured setting reads as "computed".
val args = call.argument.filter(_.argumentIndex != 0).l
ujson.Obj(
"setting" -> call.code.take(200),
"line" -> call.lineNumber.getOrElse(-1).toString,
"method" -> call.method.name,
"args" -> ujson.Arr(args.map(a => ujson.Str(a.code.take(80))): _*),
// A keyword argument (`origins = "*"`) is an assignment-shaped node, so `isLiteral` is false on the
// argument itself and the literal sits inside it. Collect literals from the argument's SUBTREE, or
// every keyword-configured setting reads as "computed" and nothing is ever evaluated.
"literalArgs" -> ujson.Arr(args.flatMap(a => a.ast.isLiteral.code.l).distinct.map(c => ujson.Str(c.take(80))): _*)
)
}
println("---OUSAST-CPG-BEGIN---")
println(ujson.write(ujson.Arr(rows: _*)))
println("---OUSAST-CPG-END---")
}
+1
View File
@@ -35,6 +35,7 @@ SPEC_OWNED = {
"model.taint": "model-grounded-detection Req 6.1: taint reachability over the CPG, the flow-family arbiter",
"model.judge": "model-grounded-detection Req 8: one bounded question, checked against the model",
"model.dominance": "model-grounded-detection Req 6.2: guard dominance, the arbiter for bugs that never crash",
"model.config_value": "model-grounded-detection Req 6.3: constant abstraction for the configuration families",
"cpg": "model-grounded-detection Req 4: the CPG seam package",
"cpg.backend": "model-grounded-detection Req 4.1/4.5: the single Joern subprocess boundary",
"cpg.capability": "model-grounded-detection Req 4.2/4.3: the engine probe that degrades to suspicion",
+77
View File
@@ -0,0 +1,77 @@
"""Constant abstraction for the configuration families (model-grounded-detection, Req 6.3).
The third arbiter form, and the simplest. A configuration bug has no flow to follow and no guard to dominate:
``CORS(app, origins="*")`` is dangerous because of the value it is *set to*. So the model evaluates the
setting's arguments against the closed permissive set from the obligation facts.
ENTAILED a security setting is passed a literal in the permissive set — the model can read the value
CORROBORATED the setting's argument is computed, so constant abstraction cannot decide it; whether the
computed value is permissive is the residual the judge asks about
(none) every literal argument is outside the permissive set, or the call is not a known setting
"""
from __future__ import annotations
from collections.abc import Mapping, Sequence
from ..cpg.backend import CpgResult
from .ladder import Rung, Verdict
from .specs import ConfigSpec
def verdict(cpg: CpgResult, spec: ConfigSpec, *, function: str = "") -> Verdict | None:
"""The verdict for a security setting in ``function``, or ``None`` when nothing is established."""
rows = cpg.run("config", {"settings": spec.settings, "function": function})
settings = _settings(rows, spec, function=function)
if not settings:
return None
permissive = {value.strip().strip("'\"").lower() for value in spec.permissive}
for row in settings:
for literal in row["literals"]:
if str(literal).strip().strip("'\"").lower() in permissive:
return Verdict(
rung=Rung.ENTAILED,
family=spec.family,
witness=f"{row['setting']} (line {row['line']}): permissive literal {literal}",
)
# A setting whose value the model could not read. Not safe, not decided.
computed = [row for row in settings if not row["literals"]]
if computed:
row = computed[0]
return Verdict(
rung=Rung.CORROBORATED,
family=spec.family,
witness=f"{row['setting']} (line {row['line']}): value is computed, not a literal the model can evaluate",
)
return None
def _settings(rows: object, spec: ConfigSpec, *, function: str) -> list[dict[str, object]]:
"""Rows for calls this spec actually names. A non-list (an engine 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
setting = str(row.get("setting", ""))
if not any(name in setting for name in spec.settings):
continue # a permissive literal passed to something that is not a security setting is not a bug
if function and str(row.get("method", "")) != function:
continue
literals = row.get("literalArgs")
kept.append(
{
"setting": setting[:200],
"line": str(row.get("line", "")),
"method": str(row.get("method", "")),
"literals": [str(v) for v in literals] if isinstance(literals, Sequence) and not isinstance(literals, str) else [],
}
)
# Deterministic order: the first row decides, so it must not depend on engine ordering.
return sorted(kept, key=lambda row: (str(row["method"]), str(row["line"]), str(row["setting"])))
__all__ = ["verdict"]
+31
View File
@@ -122,6 +122,35 @@ class DominanceSpec:
dischargers: tuple[str, ...]
@dataclass(frozen=True)
class ConfigSpec:
"""What a configuration family's constant-abstraction query matches on, for one language.
A config bug has no flow and no guard: ``CORS(app, origins="*")`` is dangerous because of the value it is
*set to*, not because anything reaches it. So the arbiter evaluates the argument against a closed
permissive set — the third form beside taint reachability and guard dominance.
"""
family: str
language: str
settings: tuple[str, ...]
permissive: tuple[str, ...]
def config_specs(*, language: str, facts: ObligationFacts | None = None) -> Mapping[str, ConfigSpec]:
"""One ``ConfigSpec`` per configuration family, from the obligation facts' security-setting operations.
``permissive_values`` on a ``non_permissive_value`` discharger is the closed set of literals that leave a
setting open — the one place in the facts where a value, rather than a call, is the evidence.
"""
scoped = (facts if facts is not None else load_obligation_facts()).for_language(language)
settings = tuple(sorted({call for fact in scoped.operations if fact.kind == "security_setting" for call in fact.calls}))
permissive = tuple(sorted({value for fact in scoped.dischargers for value in fact.permissive_values}))
if not settings or not permissive:
return {}
return {"config_secrets": ConfigSpec(family="config_secrets", language=language, settings=settings, permissive=permissive)}
def taint_specs(
*,
language: str,
@@ -184,6 +213,8 @@ def dominance_specs(*, language: str, facts: ObligationFacts | None = None) -> M
__all__ = [
"GUARD_KINDS",
"ConfigSpec",
"config_specs",
"GUARD_PATTERNS",
"DominanceSpec",
"TaintSpec",
+78
View File
@@ -0,0 +1,78 @@
"""model-grounded-detection task 4.2 / Req 6.3: the configuration arm is constant abstraction, not taint.
A config_secrets bug has no flow and no guard. `CORS(app, origins="*")` is not dangerous because untrusted
input reaches it — nothing reaches it — but because the value it is *set to* is permissive. So the arbiter is
abstract-value evaluation of the argument against the closed permissive set, which is the third form the
design names alongside taint reachability and guard dominance.
"""
from __future__ import annotations
from pathlib import Path
def _spec(): # type: ignore[no-untyped-def]
from openultrasast.model.specs import ConfigSpec
return ConfigSpec(family="config_secrets", language="python",
settings=("CORS", "set_cookie", "app.run"),
permissive=("'*'", '"*"', "True", "0.0.0.0"))
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)
def test_a_permissive_literal_is_entailed() -> None:
from openultrasast.model.config_value import verdict
from openultrasast.model.ladder import Rung
rows = [{"setting": 'CORS(app, origins="*")', "line": "3", "method": "create_app",
"literalArgs": ['"*"'], "args": ["app", '"*"']}]
answer = verdict(_cpg(rows), _spec(), function="create_app")
assert answer is not None and answer.rung is Rung.ENTAILED
assert "*" in answer.witness and "CORS" in answer.witness
def test_a_restrictive_literal_yields_no_finding() -> None:
from openultrasast.model.config_value import verdict
rows = [{"setting": 'CORS(app, origins="https://app.example")', "line": "3", "method": "create_app",
"literalArgs": ['"https://app.example"'], "args": ["app", '"https://app.example"']}]
assert verdict(_cpg(rows), _spec(), function="create_app") is None
def test_a_non_literal_argument_is_corroborated_not_entailed() -> None:
"""The value is computed, so constant abstraction cannot decide it — that residual is the LLM's."""
from openultrasast.model.config_value import verdict
from openultrasast.model.ladder import Rung
rows = [{"setting": "CORS(app, origins=configured_origins)", "line": "3", "method": "create_app",
"literalArgs": [], "args": ["app", "configured_origins"]}]
answer = verdict(_cpg(rows), _spec(), function="create_app")
assert answer is not None and answer.rung is Rung.CORROBORATED
def test_a_setting_that_is_not_in_the_spec_is_ignored() -> None:
from openultrasast.model.config_value import verdict
rows = [{"setting": 'print("*")', "line": "1", "method": "create_app", "literalArgs": ['"*"'], "args": ['"*"']}]
assert verdict(_cpg(rows), _spec(), function="create_app") is None
def test_a_failed_query_is_not_read_as_a_safe_configuration() -> None:
from openultrasast.cpg.backend import CpgResult
from openultrasast.model.config_value import verdict
assert verdict(CpgResult(cpg_path=Path("c.bin"), run=lambda q, p: None), _spec(), function="f") is None
def test_the_verdict_is_deterministic() -> None:
from openultrasast.model.config_value import verdict
rows = [{"setting": 'set_cookie("s", secure=False)', "line": "9", "method": "login",
"literalArgs": ["True", "0.0.0.0"], "args": ["x"]}]
cpg = _cpg(rows)
assert verdict(cpg, _spec(), function="login") == verdict(cpg, _spec(), function="login")