Files
Open_Ultra_SAST/tests/test_learn_mechanisms.py
T

290 lines
13 KiB
Python
Raw Normal View History

"""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}