mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-16 11:49:28 +00:00
src/openultrasast/source.py replaces the copies in semantic/functions.py, plane/tasks/repo_facts.py, learn/features.py, learn/excerpt.py and plane/tasks/verify.py. Two engines share one sentinel (<module>), one DECLARATION/KEYWORDS/RESERVED set and one alias map: the parser engine (ranges, enclosing, bounds, call_sites, callers_of, facts_for) for the scorer, the overlay and the hunter's annotations; the line-pattern engine (declared_functions, enclosing_at, function_span, function_bounds, the brace matcher) that labels, features, roles, excerpts and plane facts were built on. learn no longer imports plane.tasks. Frozen records that store the old sentinel are mapped on read (module_function). Gate: labels rebuilt before and after are identical, 7,921 rows, after sentinel normalisation (benchmarks/measurements/2026-10-13-source-module- label-diff/record.json). 490 targeted tests pass; 3,312 collect. Net: 657 lines deleted, 212 added. benchmarks/unseen keeps <global> for its own frozen manifests. Co-Authored-By: Claude Fable 5.1 <[email protected]>
128 lines
6.8 KiB
Python
128 lines
6.8 KiB
Python
"""The decision engine's harvest Runs (learned-decision-engine task 5): `ousast plane harvest`'s generator."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import subprocess
|
|
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
|
|
import pytest
|
|
import yaml
|
|
|
|
from openultrasast.plane import reconciler
|
|
from openultrasast.plane.harvest import MAX_INLINE_BYTES, Unit, blob_sha1, declaration_line, render_harvest
|
|
from openultrasast.plane.manifests import load_manifests
|
|
|
|
PLANE = Path("plane")
|
|
PIN = "0123456789abcdef0123456789abcdef01234567"
|
|
|
|
|
|
def _templates() -> dict:
|
|
return load_manifests([PLANE / "tasks" / f"{n}.yaml" for n in ("repo-facts", "verify", "agree", "roles")]).tasks
|
|
|
|
|
|
def _units() -> list[Unit]:
|
|
code = "import os\n\ndef run(cmd):\n os.system(cmd)\n"
|
|
excerpt = Unit("h-aaaaaaaaaaaa", "pairs", "x-cwe-078-os-system", "vulnerable", "pairs/github/x", blob_sha1(code), "injection",
|
|
(("app.py", "run", 3),), (("app.py", code),)) # fmt: skip
|
|
roles_only = Unit("h-bbbbbbbbbbbb", "pairs", "y-cwe-416", "fixed", "pairs/vfc/y", blob_sha1("int f(){}\n"), "",
|
|
(("f.c", "f", 1),), (("f.c", "int f(){}\n"),)) # fmt: skip
|
|
big = "x = 1\n" * (MAX_INLINE_BYTES // 6 + 10)
|
|
oversized = Unit(
|
|
"h-cccccccccccc",
|
|
"pairs",
|
|
"z",
|
|
"vulnerable",
|
|
"pairs/github/z",
|
|
blob_sha1(big),
|
|
"injection",
|
|
(("big.py", GLOBAL_, 1),),
|
|
(("big.py", big),),
|
|
)
|
|
repo = Unit("h-dddddddddddd", "population-v1", "case", "vulnerable", "https://github.com/o/r", PIN, "injection",
|
|
(("a.py", "q", 10), ("b.py", "r", 20)), sizes=(("a.py", 4000, 100), ("b.py", 8000, 900))) # fmt: skip
|
|
return [excerpt, roles_only, oversized, repo]
|
|
|
|
|
|
GLOBAL_ = "<module>"
|
|
|
|
|
|
def _render(tmp_path: Path, ceiling: float = 10.0) -> tuple[dict[str, str], Path]:
|
|
files = render_harvest(_units(), _templates(), "hv", "test", ceiling=ceiling)
|
|
for rel, text in files.items():
|
|
(tmp_path / rel).parent.mkdir(parents=True, exist_ok=True)
|
|
(tmp_path / rel).write_text(text)
|
|
(tmp_path / "models").mkdir()
|
|
(tmp_path / "models" / "deepseek-flash.yaml").write_text((PLANE / "models" / "deepseek-flash.yaml").read_text())
|
|
return files, tmp_path
|
|
|
|
|
|
def test_verify_run_shape_and_ceiling(tmp_path: Path) -> None:
|
|
files, root = _render(tmp_path, ceiling=0.05)
|
|
run, manifests = reconciler.load_run(root / "runs" / "hv-verify.yaml")
|
|
names = [t.name for t in run.tasks]
|
|
assert names == [
|
|
"h-aaaaaaaaaaaa-va", "h-aaaaaaaaaaaa-vb", "h-aaaaaaaaaaaa-agree",
|
|
"h-dddddddddddd-facts", "h-dddddddddddd-va", "h-dddddddddddd-vb", "h-dddddddddddd-agree",
|
|
] # fmt: skip
|
|
assert sum(float(t.budget.usd) for t in run.tasks) <= 0.05 # scaled down to the ceiling
|
|
assert all(float(t.budget.usd) == 0 for t in run.tasks if t.name.endswith(("-agree", "-facts")))
|
|
excerpt = manifests.tasks["verify-a-h-aaaaaaaaaaaa"]
|
|
env = {e.name: e.value for e in excerpt.env}
|
|
assert env["OUSAST_WORKSPACE_DIR"].endswith("/repo") and "/inputs/" in env["OUSAST_INPUT_CANDIDATES"]
|
|
workspace = manifests.workspaces["h-aaaaaaaaaaaa-side"]
|
|
paths = {f.path for f in workspace.files}
|
|
assert paths == {"repo/app.py", "inputs/candidates.json"} # candidates outside the hunt's root
|
|
assert "cwe" not in yaml.safe_dump([workspace.metadata.name, *paths]) # the pair's name never reaches the sandbox
|
|
units = json.loads(files["units.json"])
|
|
assert "skipped" in units["h-cccccccccccc"] and "skipped" not in units["h-aaaaaaaaaaaa"]
|
|
agree = {e.name: e.value for e in manifests.tasks["agree-h-dddddddddddd"].env}
|
|
assert "OUSAST_INPUT_SITES" not in agree # no declared sites: no site_match is computed
|
|
|
|
|
|
def test_roles_run_groups_excerpts_and_pins(tmp_path: Path) -> None:
|
|
_, root = _render(tmp_path)
|
|
run, manifests = reconciler.load_run(root / "runs" / "hv-roles.yaml")
|
|
assert len(run.tasks) == 2 and sum(float(t.budget.usd) for t in run.tasks) <= 10.0
|
|
grouped = next(w for w in manifests.workspaces.values() if w.metadata.name.startswith("hv-roles"))
|
|
listing = json.loads(next(f.content for f in grouped.files if f.path == "inputs/files.json"))
|
|
assert listing == ["h-aaaaaaaaaaaa/app.py", "h-bbbbbbbbbbbb/f.c"] # the oversized excerpt is left out
|
|
assert sum(len(f.content) for f in grouped.files) <= MAX_INLINE_BYTES + 200
|
|
pin = manifests.tasks["roles-h-dddddddddddd"]
|
|
assert {e.name: e.value for e in pin.env}["OUSAST_INPUT_FILES"].endswith("/files.json")
|
|
|
|
|
|
def test_blob_sha1_is_gits_and_declaration_lines() -> None:
|
|
text = "def a():\n pass\n\ndef b():\n pass\n"
|
|
done = subprocess.run(["git", "hash-object", "--stdin"], input=text, capture_output=True, text=True, check=True)
|
|
assert blob_sha1(text) == done.stdout.strip()
|
|
lines = text.splitlines()
|
|
assert declaration_line(lines, "m.py", "b") == 4
|
|
assert declaration_line(lines, "m.py", "missing") == 1 and declaration_line(None, "m.py", "a") == 1
|
|
|
|
|
|
def test_pair_sides_ask_only_the_functions_they_declare(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
"""A fixed side whose fix removed a labelled function never gets it as a candidate (no phantom verify candidate);
|
|
it is listed under ``absent_on_side``. A side declaring none of its verify family's functions is no verify unit."""
|
|
from openultrasast import pairs
|
|
from openultrasast.plane.harvest import pair_units
|
|
|
|
vuln, fixed = tmp_path / "vuln.py", tmp_path / "fixed.py"
|
|
vuln.write_text("import os\n\ndef run(cmd):\n os.system(cmd)\n\ndef keep(x):\n return x\n")
|
|
fixed.write_text("import shlex\n\ndef keep(x):\n return shlex.quote(x)\n")
|
|
case = SimpleNamespace(name="p-cwe-078", vuln_file=vuln, fixed_file=fixed, relpath="app.py", context_files=(), repo="", slice="s")
|
|
monkeypatch.setattr(pairs, "load_pair_catalog", lambda _catalog: [case])
|
|
rows = [{"source": "pairs", "unit": "pin", "source_ref": "p-cwe-078", "family": "injection", "candidate": f"app.py::{fn}"}
|
|
for fn in ("run", "keep")] # fmt: skip
|
|
labels = tmp_path / "labels.jsonl"
|
|
labels.write_text("".join(json.dumps(r) + "\n" for r in rows))
|
|
by_side = {u.side: u for u in pair_units(labels, tmp_path / "catalog.toml")}
|
|
assert [c[1] for c in by_side["vulnerable"].candidates] == ["keep", "run"] and by_side["vulnerable"].absent_on_side == ()
|
|
assert [c[1] for c in by_side["fixed"].candidates] == ["keep"] and [c[1] for c in by_side["fixed"].verify] == ["keep"]
|
|
assert by_side["fixed"].absent_on_side == ("app.py::run",)
|
|
assert by_side["fixed"].index_row()["absent_on_side"] == ["app.py::run"]
|
|
fixed.write_text("import shlex\n\ndef other(x):\n return shlex.quote(x)\n")
|
|
gone = next(u for u in pair_units(labels, tmp_path / "catalog.toml") if u.side == "fixed")
|
|
assert gone.candidates == () and gone.verify == () and gone.family == "" and len(gone.absent_on_side) == 2
|