mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-16 03:49:26 +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
@@ -626,3 +626,56 @@ def test_the_taint_query_streams_one_line_per_request(tmp_path: Path) -> None:
|
||||
answers = [json.loads(line) for line in lines[1:]]
|
||||
assert all(isinstance(a.get("ms"), (int, float)) and a["ms"] >= 0 for a in answers), answers
|
||||
assert [t["id"] for t in assembled["__timing__"]] == ["0", "1"]
|
||||
|
||||
|
||||
@pytest.mark.skipif(_joern() is None, reason="joern is not installed on this machine")
|
||||
def test_context_locations_outside_the_changed_files_are_summarised(tmp_path: Path) -> None:
|
||||
"""Only the changed files are itemised; every other location collapses into one checkable summary.
|
||||
|
||||
Itemised, one PHP request returned 1,845 locations and a push over a WordPress plugin never finished.
|
||||
"""
|
||||
frontend = __import__("shutil").which("jssrc2cpg")
|
||||
if frontend is None:
|
||||
pytest.skip("jssrc2cpg is not installed")
|
||||
(tmp_path / "src").mkdir()
|
||||
(tmp_path / "src" / "a.js").write_text(
|
||||
"const h = require('./b');\nfunction handler(req, res) { return h.helper(req.body.x); }\nmodule.exports = handler;\n"
|
||||
)
|
||||
(tmp_path / "src" / "b.js").write_text("function helper(x) { return eval(x); }\nmodule.exports = { helper };\n")
|
||||
cpg = tmp_path / "cpg.bin"
|
||||
subprocess.run([frontend, str(tmp_path / "src"), "-o", str(cpg)], capture_output=True, text=True, timeout=600, check=False)
|
||||
if not cpg.is_file():
|
||||
pytest.skip("could not build a sample cpg")
|
||||
# Repository-wide scope, so the context reaches methods in both files. A `require`d helper is not a
|
||||
# resolved call edge in the JavaScript graph, so a handler-scoped request would never leave `a.js`.
|
||||
request = {"sources": "req.body", "sinks": "eval", "evidenceOnly": "true", "contextEvidence": "true"}
|
||||
requests = tmp_path / "requests.json"
|
||||
requests.write_text(json.dumps({"whole": request, "filtered": {**request, "contextFilter": "true", "contextPaths": "b.js"}}))
|
||||
done = subprocess.run(
|
||||
[
|
||||
_joern() or "joern",
|
||||
"--script",
|
||||
str((QUERIES / "taint.sc").resolve()),
|
||||
"--param",
|
||||
f"cpgFile={cpg}",
|
||||
"--param",
|
||||
f"requestsFile={requests}",
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=900,
|
||||
check=False,
|
||||
)
|
||||
assert done.returncode == 0, (done.stderr or done.stdout or "")[-500:]
|
||||
from openultrasast.cpg.backend import extract_payload
|
||||
|
||||
payload = extract_payload(done.stdout)
|
||||
assert isinstance(payload, dict)
|
||||
whole = [r for r in payload["whole"] if r.get("kind") == "context_method"]
|
||||
kept = [r for r in payload["filtered"] if r.get("kind") == "context_method"]
|
||||
summary = [r for r in payload["filtered"] if r.get("kind") == "context_elsewhere"]
|
||||
assert {r["path"] for r in whole} >= {"a.js", "b.js"}, whole # the unfiltered form still itemises everything
|
||||
assert kept and all(r["path"] == "b.js" for r in kept), kept
|
||||
assert kept == [r for r in whole if r["path"] == "b.js"], "the changed file's locations must be unchanged"
|
||||
assert len(summary) == 1 and summary[0]["paths"] == sorted({r["path"] for r in whole if r["path"] != "b.js"})
|
||||
assert summary[0]["locations"] == len(whole) - len(kept)
|
||||
|
||||
Reference in New Issue
Block a user