mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-19 04:59:28 +00:00
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:
co-authored by
Claude Opus 5
parent
fec67253f3
commit
d9ceeb6a09
@@ -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"]
|
||||
Reference in New Issue
Block a user