Files
Open_Ultra_SAST/src/openultrasast/model/config_value.py
T

88 lines
4.0 KiB
Python
Raw Normal View History

"""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 request_params(spec: ConfigSpec, *, function: str = "") -> dict[str, object]:
"""The query parameters this arbiter sends. Shared with the batcher; see the note in `taint.request_params`."""
return {"settings": spec.settings, "function": function}
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", request_params(spec, function=function))
settings = _settings(rows, spec, function=function)
if not settings:
return None
# Both abstractions read the same literal: a permissive flag ("*", True) and a weak algorithm ("md5").
permissive = {value.strip().strip("'\"").lower() for value in spec.permissive}
permissive |= {value.strip().strip("'\"").lower() for value in spec.weak_algorithms}
for row in settings:
literals = row["literals"]
if not isinstance(literals, list):
continue
for literal in 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"]] # type: ignore[truthy-iterable]
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__ = ["request_params", "verdict"]