2026-09-08 15:00:41 +02:00
|
|
|
"""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
|
|
|
|
|
|
2026-09-08 18:43:33 +02:00
|
|
|
# Both abstractions read the same literal: a permissive flag ("*", True) and a weak algorithm ("md5").
|
2026-09-08 15:00:41 +02:00
|
|
|
permissive = {value.strip().strip("'\"").lower() for value in spec.permissive}
|
2026-09-08 18:43:33 +02:00
|
|
|
permissive |= {value.strip().strip("'\"").lower() for value in spec.weak_algorithms}
|
2026-09-08 15:00:41 +02:00
|
|
|
for row in settings:
|
2026-09-08 15:01:08 +02:00
|
|
|
literals = row["literals"]
|
|
|
|
|
if not isinstance(literals, list):
|
|
|
|
|
continue
|
|
|
|
|
for literal in literals:
|
2026-09-08 15:00:41 +02:00
|
|
|
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.
|
2026-09-08 15:01:08 +02:00
|
|
|
computed = [row for row in settings if not row["literals"]] # type: ignore[truthy-iterable]
|
2026-09-08 15:00:41 +02:00
|
|
|
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"]
|