mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-16 19:59:26 +00:00
contributor-scan 2.2. VAmPI at a pinned commit, scanned whole rather than as excerpts, with benchmarks/repos/measure.py recording build time, query time, arbitration time, peak memory, cost, unjudged regions, and whether each known vulnerability was found and at which rung. The corpus could not have found any of this. Every defect below needs a repository to become visible, and three of them were invisible behind a passing test suite. **The batch path was unreachable.** query_batch was implemented, tested and committed while CpgResult exposed only `run`, so the driver's capability check found nothing and fell back to one JVM per region-family: the first attempt at this measurement ran eighteen minutes on 520 lines, issuing `--param function=` one at a time. Every test called query_batch directly, so none of them noticed. CpgResult now carries run_batch and a test asserts the capability is reachable through the object the driver is handed. The earlier 275s->106s improvement attributed to batching came from region deduplication alone. **The budget stopped free work.** It bounds model calls, but was enforced by breaking the region loop -- spending a paid limit to stop unpaid CPG arbitration. 6 of 22 regions were examined and VAmPI's BOLA, which the graph entails for nothing, was never reached. It now withholds the judge only. 22 of 22 regions arbitrated, 17 counted as regions_unasked (silent because unaffordable is not silent because decided), and the BOLA is entailed by the model layer. **A region with no function asked about the whole repository.** Sending function="" matched every hit in the tree and the driver attributed that answer to whoever asked: one `host='0.0.0.0'` in app.py became eight identical entailed findings in eight files that do not contain it. Queries now carry the region's file. Entailed findings 14 -> 7, query time 70.9s -> 40.9s. The single-request form stays unscoped so the committed pair measurements remain reproducible. **The JVM had no heap bound.** Joern claimed a quarter of physical RAM and runs were killed under memory pressure at different points, so the numbers described the host rather than the engine. -J-Xmx2048m by default, OPENULTRASAST_CPG_HEAP_MB to raise it. Also: arbitrated findings carried `function:line` where the report layer read `path:line`, so every entailed finding reached a contributor with a function name where its file belonged; and the witness line was parsed by splitting on a comma, which produced sites like `config.py:17): permissive literal '0.0.0.0':?`. Reading: GO on one CPG per repository -- 6.2s to build, 177s end to end, 869MB, $0.0067, nothing infeasible. NO-GO on region-scoped taint as the only shape: VAmPI's SQL injection runs from a handler parameter into a function in another module, and a per-function region sees neither end. That is the ordinary shape of a real injection bug, so it is recorded as task 2.4 along with three smaller defects this run exposed. Gates byte-identical. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01T7XD7x5KonMdmGRkp63EQG
94 lines
4.5 KiB
Python
94 lines
4.5 KiB
Python
"""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 = "", file: str = "") -> dict[str, object]:
|
|
"""The query parameters this arbiter sends. Shared with the batcher; see the note in `taint.request_params`.
|
|
|
|
``file`` scopes the answer to one source file. A region with no enclosing function sends ``function=""``,
|
|
which without this matches the whole repository and gets that answer attributed to it -- one permissive
|
|
literal in `app.py` became eight identical entailed findings in eight files that do not contain it. Empty
|
|
means unscoped, which is what the single-region pair path still uses.
|
|
"""
|
|
return {"settings": spec.settings, "function": function, "file": file}
|
|
|
|
|
|
def verdict(cpg: CpgResult, spec: ConfigSpec, *, function: str = "", file: 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, file=file))
|
|
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"]
|