Files
Open_Ultra_SAST/tests/test_plane_harvest.py
T
Marius CiepluchandClaude Opus 5.5 9e3c83cfdc fix(learn): withhold label-tracking signals from the v1 classifier; leak audit
Maintainer decision "Neutralise both for v1".

1. Phantom candidates. A pair side's candidates are now only the functions that
   side declares (plane/harvest.py `declares`, the excerpt builder's
   function_bounds); a labelled function absent from a side is listed under
   `absent_on_side` and never asked, and a side declaring none of its verify
   family's functions is no verify unit. The recorded harvest units held 58
   phantom verify candidates (53 fixed, 5 vulnerable sides). build_features.py
   skips label rows whose side does not declare the function and drops their
   earlier rows (MemoryStore.drop_rows); rebuilt the pair records from the
   recorded signals, no model call: 126 label rows absent_on_side (117 fixed,
   9 vulnerable) -- access_control 13, config_secrets 4, deserialization 20,
   injection 28, memory 4, output_encoding 16, path 15, untrusted_destination
   26 -- so 252 feature rows dropped (1712 -> 1460); no other row changed.

2. Engine withheld. Input profiles in learn/schema.py (`v1` withholds the
   engine instrument, `full` withholds nothing); ProgramSpec.inputs defaults to
   v1 and compile.toml records it. Neither the engine's state nor its values
   reach the candidate, examples, demonstrations or the instruction proposer's
   error summary, and the Gower retrieval distance leaves the engine block out.
   Stored records are unchanged. design.md Risks records the decision and that
   the engine returns only through a pre-registered A/B on assumed-benign and
   benign-control negatives.

3. `ousast learn audit-leaks` (learn/leaks.py): per feature and per instrument
   state, separation = (pairs the signal alone puts on the right side - on the
   wrong side) / pairs, over all pairs and per family; flagged at >= 0.20 over
   >= 10 pairs. Counts under
   benchmarks/measurements/2026-10-01-decision-engine-leak-audit/ (before and
   after the rebuild). After: facts.function_lines (0.80 untrusted_destination,
   0.75 config_secrets, 0.26 injection; higher on fixed), verify.turns_mean
   0.67 config_secrets, eng.* 0.35-0.40 output_encoding, verify.flag_a/votes/
   site_offset and agree.final 0.20-0.30 on 10-12 pair families. Before the
   rebuild the phantom fixed sides drove state.source/state.roles/ms.flagged
   to 1.0 and state.engine to 0.95 on deserialization.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
2026-10-01 19:40:58 +02:00

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_ = "<global>"
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