mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-18 12:39:28 +00:00
fix(scan): itemise change context only in the files a change touched
The PHP transfer replay (pre-push 13.3) spent its whole 7,200 s case in the head side's evidence pass. A probe on one PMPro graph named the cause: not compute -- context adds ~20% engine time -- but OUTPUT. Each evidence request listed every method its scope reached, up to 1,845 rows; 200 requests into the bundled Braintree SDK returned 52,831 rows, 9.4 MB, against 211 rows without context, over 22,125 requests in a 3 GB container. `affected_context` only ever attaches a location in a changed file, and every other row was checked (real file under the root, usable extent) and discarded. The scan now sends the changed files; the engine itemises those and folds the rest into one `context_elsewhere` row carrying exactly what that check reads. With no safe file list the engine itemises everything, as before. Equivalence: unit controls over valid, missing-file, no-extent and outside-root cases, with and without a changed-file row; a real-Joern control; and on PMPro itself, 2,440 relationships and zero gaps decided identically from both forms, 52,831 rows down to 3,241 with the most referenced file as the change. Probe: benchmarks/push/context_cost_probe.py. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
cda679cb3b
commit
3c195c9369
@@ -386,3 +386,68 @@ def test_a_gap_in_another_partition_does_not_demote_this_one(tmp_path):
|
||||
assert php, result.question_outcomes
|
||||
if all(str(d.get("census_language") or "") != "php" for d in result.degradations):
|
||||
assert all(o.status == "completed" for o in php), [(o.status, o.reason) for o in php]
|
||||
|
||||
|
||||
def _summary(rows):
|
||||
"""What the engine now emits in place of itemised rows for files the change did not touch."""
|
||||
unusable = sum(1 for r in rows if int(r["startLine"]) < 1 or int(r["endLine"]) < int(r["startLine"]))
|
||||
return {"kind": "context_elsewhere", "locations": len(rows), "unusableExtents": unusable, "paths": sorted({r["path"] for r in rows})}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"elsewhere",
|
||||
[
|
||||
pytest.param([row("other.php"), row("other.php", "field", "f"), row("lib/deep.php", "hook", "h")], id="valid"),
|
||||
pytest.param([row("other.php"), row("missing.php")], id="missing-file"),
|
||||
pytest.param([row("other.php"), {**row("other.php", function="g"), "startLine": -1}], id="no-extent"),
|
||||
pytest.param([row("../outside.php")], id="outside-root"),
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize("with_changed_row", [True, False])
|
||||
def test_summarised_locations_decide_exactly_what_itemised_ones_did(tmp_path, elsewhere, with_changed_row):
|
||||
"""Summarising unchanged-file locations in the engine must change nothing `affected_context` decides.
|
||||
|
||||
Itemised, one PHP request returned 1,845 locations and a push over a WordPress plugin never finished;
|
||||
every location outside the changed files was only ever checked and then discarded, so the summary carries
|
||||
exactly what that check reads.
|
||||
"""
|
||||
from openultrasast.model.contracts import QuestionIdentity
|
||||
from openultrasast.model.regions import affected_context
|
||||
|
||||
(tmp_path / "source.php").write_text("readable\nchanged\n")
|
||||
(tmp_path / "other.php").write_text("unchanged\n")
|
||||
(tmp_path / "lib").mkdir()
|
||||
(tmp_path / "lib" / "deep.php").write_text("unchanged\n")
|
||||
(tmp_path.parent / "outside.php").write_text("outside the root\n")
|
||||
question = QuestionIdentity("unit", "php", "sink.php", "run", "injection")
|
||||
changed = [row("source.php", "callee")] if with_changed_row else []
|
||||
head = [{"kind": "context_summary"}]
|
||||
itemised = head + changed + elsewhere
|
||||
summarised = head + changed + [_summary(elsewhere)]
|
||||
old = affected_context(context(), [question], {question: itemised}, tmp_path)
|
||||
new = affected_context(context(), [question], {question: summarised}, tmp_path)
|
||||
assert new == old
|
||||
|
||||
|
||||
def test_the_evidence_pass_names_the_changed_files_to_the_engine(tmp_path):
|
||||
"""The engine can only summarise what the driver says is unchanged; with no safe list it itemises all."""
|
||||
from openultrasast.model import scan
|
||||
|
||||
asked: list[dict[str, object]] = []
|
||||
|
||||
class Graph:
|
||||
cpg_path = tmp_path / "cpg.bin"
|
||||
|
||||
def run_batch(self, kind, requests):
|
||||
asked.extend(requests.values())
|
||||
return {rid: [] for rid in requests}
|
||||
|
||||
work = scan._collect([region("sink.php", "php")])
|
||||
context_rows: dict = {}
|
||||
scan._evidence_pass(Graph(), work, "", context_rows=context_rows, context_files=("a.php", "b,c.php"))
|
||||
assert asked and all(r["contextFilter"] == "true" and r["contextPaths"] == "a.php\nb,c.php" for r in asked)
|
||||
asked.clear()
|
||||
scan._evidence_pass(Graph(), work, "", context_rows=context_rows, context_files=None)
|
||||
assert asked and all("contextFilter" not in r for r in asked)
|
||||
assert scan._changed_files(context("x\ny.php")) is None
|
||||
assert scan._changed_files(context("source.php")) == ("source.php",)
|
||||
|
||||
Reference in New Issue
Block a user