mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-16 03:49:26 +00:00
Uncommitted work from the fix-mechanisms worktree (mechanisms.py + cst/extra/cli edits), committed so the worktree can be removed without loss. Branch is 0 commits on top of a 172-behind base; re-integrate via rebase if resumed. Co-Authored-By: Claude Opus 4.8 <[email protected]>
290 lines
13 KiB
Python
290 lines
13 KiB
Python
"""Authored fix pairs only: no store, models, containers, or population files."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import importlib.util
|
|
import json
|
|
from dataclasses import replace
|
|
|
|
import pytest
|
|
|
|
from openultrasast.learn.folds import Fold
|
|
from openultrasast.learn.mechanisms import Pair, mine_pairs
|
|
|
|
|
|
def pair(before: str, after: str, *, language: str = "python", family: str = "output_encoding", repo: str = "owner/alpha") -> Pair:
|
|
return Pair("pair-" + repo, repo, repo, language, family, before, after)
|
|
|
|
|
|
def repeated(p: Pair, **kwargs):
|
|
return mine_pairs([p, replace(p, id="pair-beta", repository="owner/beta", group="owner/beta")], **kwargs)
|
|
|
|
|
|
def test_learn_mechanisms_sanitizer_return_and_def_use():
|
|
p = pair("def f(x):\n return x", "def f(x):\n y = html.escape(x)\n return y")
|
|
result = repeated(p)
|
|
assert [(c["form"], c["pattern"]) for c in result.candidates] == [("sanitizer", {"callee": "html.escape", "argument": 0})]
|
|
assert result.candidates[0]["repository_count"] == 2
|
|
assert "owner/alpha" not in json.dumps(result.record)
|
|
assert result.record["pairs_mined"] == 2
|
|
|
|
|
|
def test_learn_mechanisms_javascript():
|
|
p = pair("function f(x) { res.send(x); }", "function f(x) { const y = escapeHtml(x); res.send(y); }", language="javascript")
|
|
assert repeated(p).candidates[0]["pattern"] == {"callee": "escapeHtml", "argument": 0}
|
|
|
|
|
|
def test_learn_mechanisms_guard_and_known():
|
|
p = pair(
|
|
"def f(u):\n redirect(u)",
|
|
"def f(u):\n if not is_safe_url(u):\n abort()\n redirect(u)",
|
|
family="untrusted_destination",
|
|
)
|
|
candidate = repeated(p).candidates[0]
|
|
assert candidate["form"] == "guard"
|
|
assert candidate["pattern"] == {"callee": "is_safe_url", "argument": 0, "predicate": "truthy"}
|
|
assert candidate["known"] is True
|
|
assert not any(f["eligible"] for f in candidate["folds"])
|
|
|
|
|
|
@pytest.mark.skipif(importlib.util.find_spec("tree_sitter_php") is None, reason="PHP tree-sitter wheel unavailable")
|
|
def test_learn_mechanisms_php_safe_argument_and_echo():
|
|
p = pair(
|
|
"function f($d) { return unserialize($d); }",
|
|
"function f($d) { return unserialize($d, ['allowed_classes' => false]); }",
|
|
language="php",
|
|
family="deserialization",
|
|
)
|
|
assert repeated(p).candidates[0]["pattern"] == {"callee": "unserialize", "argument": 1, "key": "allowed_classes", "value": False}
|
|
assert repeated(p).candidates[0]["form"] == "safe_argument"
|
|
p = pair("function f($x) { echo $x; }", "function f($x) { echo htmlspecialchars($x); }", language="php")
|
|
assert repeated(p).candidates[0]["form"] == "sanitizer"
|
|
|
|
|
|
def test_learn_mechanisms_safe_alternative():
|
|
p = pair("def f(x):\n return yaml.load(x)", "def f(x):\n return yaml.safe_load(x)", family="deserialization")
|
|
candidate = repeated(p).candidates[0]
|
|
assert candidate["form"] == "safe_alternative"
|
|
assert candidate["pattern"] == {"callee": "yaml.load", "replacement": "yaml.safe_load"}
|
|
assert candidate["known"] is True
|
|
|
|
|
|
def test_learn_mechanisms_keyword():
|
|
p = pair("def f(x):\n subprocess.run(x, shell=True)", "def f(x):\n subprocess.run(x, shell=False)", family="injection")
|
|
result = repeated(p)
|
|
assert any(c["pattern"] == {"callee": "subprocess.run", "key": "shell", "value": False} for c in result.candidates)
|
|
|
|
|
|
def test_learn_mechanisms_one_repository_and_local_helpers_rejected():
|
|
p = pair("def f(x):\n return x", "def f(x):\n return cleanse(x)")
|
|
assert mine_pairs([p]).candidates == []
|
|
local = replace(p, fixed="def cleanse(x):\n return x\ndef f(x):\n return cleanse(x)")
|
|
result = repeated(local)
|
|
assert result.candidates == []
|
|
assert result.record["skips"]["local_definition"] == 2
|
|
assert repeated(replace(p, fixed="def f(x):\n return alpha_escape(x)")).candidates == []
|
|
|
|
|
|
def test_learn_mechanisms_folds_exclude_proposers():
|
|
p = pair("def f(x):\n return x", "def f(x):\n return cleanse(x)")
|
|
folds = [Fold("outer", "outer-0", frozenset({p.group})), Fold("outer", "outer-1", frozenset({"elsewhere"}))]
|
|
candidate = repeated(p, folds=folds).candidates[0]
|
|
assert [(f["repository_count"], f["eligible"]) for f in candidate["folds"]] == [(1, False), (2, True)]
|
|
assert candidate["folds"][0]["proposing_pairs"] == ["pair-beta"]
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"fixed",
|
|
[
|
|
"def f(x):\n cleanse(x)\n return x",
|
|
"def f(x):\n y = cleanse(x)\n y = 'constant'\n return y",
|
|
"def f(x):\n if is_safe_url(x):\n pass\n redirect(x)",
|
|
],
|
|
)
|
|
def test_learn_mechanisms_unrelated_calls_are_not_candidates(fixed):
|
|
before = "def f(x):\n redirect(x)" if "redirect" in fixed else "def f(x):\n return x"
|
|
family = "untrusted_destination" if "redirect" in fixed else "output_encoding"
|
|
assert repeated(pair(before, fixed, family=family)).candidates == []
|
|
|
|
|
|
def test_learn_mechanisms_skips_and_determinism():
|
|
p = pair("def f(x):\n return x", "def f(x):\n return cleanse(x)")
|
|
assert repeated(p) == repeated(p)
|
|
assert repeated(replace(p, fixed="def broken(")).record["skips"]["unparseable"] == 2
|
|
assert repeated(replace(p, language="ruby")).record["skips"]["unsupported_language"] == 2
|
|
assert (
|
|
repeated(replace(p, fixed="def f(x):\n for y in x:\n cleanse(y)\n return x")).record["skips"]["unsupported_shape"] == 2
|
|
)
|
|
|
|
|
|
def test_learn_mechanisms_guard_must_test_sink_value():
|
|
p = pair(
|
|
"def f(x, u):\n redirect(u)",
|
|
"def f(x, u):\n if not check(x):\n abort()\n redirect(u)",
|
|
family="untrusted_destination",
|
|
)
|
|
assert repeated(p).candidates == []
|
|
|
|
|
|
def test_learn_mechanisms_existing_path_call_is_not_new():
|
|
p = pair("def f(x):\n return html.escape(x)", "def f(x):\n y = html.escape(x)\n return y")
|
|
assert repeated(p).candidates == []
|
|
|
|
|
|
def test_learn_mechanisms_missing_grammar_is_counted(monkeypatch):
|
|
monkeypatch.setattr("openultrasast.learn.mechanisms.grammar_for", lambda language: None)
|
|
result = repeated(pair("def f(x):\n return x", "def f(x):\n return cleanse(x)"))
|
|
assert result.candidates == []
|
|
assert result.record["skips"] == {"grammar_unavailable": 2}
|
|
|
|
|
|
def test_learn_mechanisms_pair_identity_and_unread_inputs():
|
|
from openultrasast.learn.mechanisms import pairs_from_rows
|
|
|
|
base = {
|
|
"repo": "owner/alpha",
|
|
"group": "owner/alpha",
|
|
"source": "pairs",
|
|
"candidate": "app.py::f",
|
|
"language": "python",
|
|
"family": "output_encoding",
|
|
"profile": "static",
|
|
"unit": "pin",
|
|
}
|
|
rows = [{**base, "label": label, "excerpt_sha": str(label)} for label in (0, 1)]
|
|
pairs, skips, count = pairs_from_rows(rows, lambda sha: "def f(x):\n return x")
|
|
assert len(pairs) == count == 1 and not skips
|
|
assert pairs_from_rows(rows + [rows[0]], lambda sha: "code")[1] == {"ambiguous_pair": 1}
|
|
assert pairs_from_rows(rows[:1], lambda sha: "code")[1] == {"incomplete_pair": 1}
|
|
assert pairs_from_rows(rows, lambda sha: None)[1] == {"unreadable_excerpt": 1}
|
|
forbidden = [{**row, "source": "population-v3"} for row in rows]
|
|
assert pairs_from_rows(forbidden, lambda sha: pytest.fail("must not read excluded excerpts"))[1] == {"excluded_source": 2}
|
|
|
|
|
|
def memory_store(tmp_path):
|
|
from openultrasast.plane.memory import FileStore, row_id
|
|
|
|
store = FileStore(tmp_path / "memory")
|
|
rows = []
|
|
for repo in ("owner/alpha", "owner/beta", "owner/gamma"):
|
|
for label in (0, 1):
|
|
code = "def f(x):\n return x" if label else "def f(x):\n return cleanse(x)"
|
|
sha = store.put_blob("excerpts", code.encode())
|
|
rows.append(
|
|
{
|
|
"id": row_id("example", "test", "test", f"{repo}:{label}"),
|
|
"kind": "example",
|
|
"repo": repo,
|
|
"pin": str(label) * 40,
|
|
"run": "test",
|
|
"task": "test",
|
|
"population": "pairs",
|
|
"split": "pairs",
|
|
"image": "host",
|
|
"candidate": "app.py::f",
|
|
"family": "output_encoding",
|
|
"language": "python",
|
|
"profile": "static",
|
|
"label": label,
|
|
"source": "pairs",
|
|
"group": repo,
|
|
"unit": "pin",
|
|
"x": {},
|
|
"instruments": {},
|
|
"excerpt_sha": sha,
|
|
}
|
|
)
|
|
store.put_rows(rows)
|
|
return store
|
|
|
|
|
|
def test_learn_mechanisms_store_cli_and_privacy(tmp_path, monkeypatch):
|
|
from openultrasast.cli import main
|
|
from openultrasast.learn.mechanisms import RECORD, mine_store
|
|
|
|
store = memory_store(tmp_path)
|
|
result = mine_store(store)
|
|
assert result.record["complete_pairs"] == 3
|
|
assert result.record["excerpt_bytes_read"] > 0
|
|
assert len(result.candidates) == 1
|
|
assert len(result.candidates[0]["folds"]) == 5
|
|
monkeypatch.setenv("OUSAST_RESULTS", str(tmp_path / "results"))
|
|
root = tmp_path / "repo"
|
|
out = tmp_path / "results" / "plane" / "mining" / "candidates.jsonl"
|
|
assert main(["learn", "mechanisms", "mine", "--memory", f"file://{tmp_path / 'memory'}", "--out", str(out), "--root", str(root)]) == 0
|
|
record = (root / RECORD).read_text()
|
|
assert all(repo not in record for repo in ("alpha", "beta", "gamma"))
|
|
assert json.loads(out.read_text())["repository_count"] == 3
|
|
assert main(["learn", "mechanisms", "mine", "--memory", f"file://{tmp_path / 'empty'}", "--out", str(out), "--root", str(root)]) == 2
|
|
assert json.loads((root / RECORD).read_text())["status"] == "no_mineable_pairs"
|
|
|
|
|
|
def test_learn_mechanisms_output_location_rejected(tmp_path):
|
|
from openultrasast.learn.mechanisms import MiningResult, write_result
|
|
|
|
root = tmp_path / "repo"
|
|
with pytest.raises(ValueError, match="outside"):
|
|
write_result(MiningResult([], {}), root / "candidate.jsonl", root=root, results=tmp_path)
|
|
assert not root.exists()
|
|
|
|
|
|
@pytest.mark.parametrize("signature,source", [("", "input()"), ("request", "request.get_json()")])
|
|
def test_learn_mechanisms_source_calls(signature, source):
|
|
p = pair(f"def f({signature}):\n return {source}", f"def f({signature}):\n return html.escape({source})")
|
|
assert repeated(p).candidates[0]["pattern"] == {"callee": "html.escape", "argument": 0}
|
|
|
|
|
|
def test_learn_mechanisms_inserted_log_is_not_sink_replacement():
|
|
p = pair("def f(x):\n yaml.load(x)", "def f(x):\n logger.info(x)\n yaml.safe_load(x)", family="deserialization")
|
|
result = repeated(p)
|
|
assert result.candidates == []
|
|
assert result.record["skips"]["ambiguous_sink"] == 2
|
|
|
|
|
|
def test_learn_mechanisms_keyword_reordering_is_not_change():
|
|
p = pair(
|
|
"def f(x):\n subprocess.run(x, shell=False, timeout=10)",
|
|
"def f(x):\n subprocess.run(x, timeout=10, shell=False)",
|
|
family="injection",
|
|
)
|
|
assert repeated(p).candidates == []
|
|
|
|
|
|
def test_learn_mechanisms_legacy_pair_ids_are_repository_scoped(tmp_path):
|
|
from openultrasast.learn.mechanisms import mine_store
|
|
|
|
result = mine_store(memory_store(tmp_path))
|
|
candidate = result.candidates[0]
|
|
assert len(candidate["proposing_pairs"]) == 3
|
|
assert all(len(ids) == 2 for ids in candidate["proposing_examples"].values())
|
|
assert len({i for ids in candidate["proposing_examples"].values() for i in ids}) == 6
|
|
|
|
|
|
def test_learn_mechanisms_runtime_callable_is_unsupported():
|
|
p = pair("def f(x, clean):\n return x", "def f(x, clean):\n return clean(x)")
|
|
result = repeated(p)
|
|
assert result.candidates == []
|
|
assert result.record["skips"]["unsupported_shape"] == 2
|
|
|
|
|
|
def test_learn_mechanisms_language_support_is_not_pooled():
|
|
p = pair("def f(x):\n return x", "def f(x):\n return cleanse(x)")
|
|
js = pair("function f(x) { return x; }", "function f(x) { return cleanse(x); }", language="javascript", repo="owner/beta")
|
|
assert mine_pairs([p, js]).candidates == []
|
|
|
|
|
|
def test_learn_mechanisms_guard_inside_branch_and_numbered_excerpt():
|
|
p = pair(
|
|
" 10 def f(x):\n 11 redirect(x)",
|
|
" 20 def f(x):\n 21 if is_safe_url(x):\n 22 redirect(x)",
|
|
family="untrusted_destination",
|
|
)
|
|
candidate = repeated(p).candidates[0]
|
|
assert candidate["form"] == "guard" and candidate["pattern"]["predicate"] == "truthy"
|
|
|
|
|
|
@pytest.mark.skipif(importlib.util.find_spec("tree_sitter_php") is None, reason="PHP tree-sitter wheel unavailable")
|
|
def test_learn_mechanisms_php_receiver_normalization():
|
|
p = pair("function f($x, $helper) { echo $x; }", "function f($x, $helper) { echo $helper->escape($x); }", language="php")
|
|
assert repeated(p).candidates[0]["pattern"] == {"callee": "escape", "argument": 0}
|