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
+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",