mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-16 11:49:28 +00:00
Equality script (design section 3) records suite outcomes, `ousast pairs --json`, the python-vulnerable quick benchmark, one standard scan and `improve --dry-run` from a frozen export with the harnessx extra absent and no model reachable. Two independent records are byte-identical (determinism.json). Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
397 lines
18 KiB
Python
397 lines
18 KiB
Python
"""Equality proof for the HarnessX removal (spec harnessx-removal, design section 3, Requirement 3).
|
|
|
|
``record`` exports a frozen source tree, runs the deterministic commands with no model reachable and the
|
|
``harnessx`` extra absent, and stores their normalized outputs. ``compare`` diffs two such records and exits
|
|
non-zero on the first difference outside the exclusion list, naming the file and key.
|
|
|
|
Usage:
|
|
python benchmarks/harnessx_removal_equality.py record <out-dir> [--tree-ish HEAD] [--workdir DIR]
|
|
python benchmarks/harnessx_removal_equality.py compare <baseline-dir> <candidate-dir> \\
|
|
[--suite-map map.json] [--record comparison.json]
|
|
|
|
The instrument checks come first and fail loudly: ``harnessx`` must not be importable, ``tree_sitter`` must be,
|
|
every child must import ``openultrasast`` from the export, every input manifest must be non-empty, every
|
|
command must exit 0, and every command must produce non-zero counts (pairs, TPs, expected findings, suite tests).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import hashlib
|
|
import importlib.util
|
|
import json
|
|
import os
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import xml.etree.ElementTree as ET
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
REPO = Path(__file__).resolve().parents[1]
|
|
|
|
# Design section 3 "Comparison": timestamps, durations and run identifiers are excluded; nothing else is.
|
|
EXCLUDED_KEYS = frozenset({"runtime_seconds", "scan_id", "timestamp", "started", "finished", "created_at", "generated_at"})
|
|
EXCLUDED_SUFFIXES = ("_seconds", "_ms")
|
|
RUN_ID = re.compile(r"\d{8}T\d{6}Z-[0-9a-f]{8}")
|
|
EXCLUSIONS = [
|
|
f"JSON keys: {', '.join(sorted(EXCLUDED_KEYS))}",
|
|
f"JSON keys ending in: {', '.join(EXCLUDED_SUFFIXES)}",
|
|
"run directory names / run ids (<timestamp>-<8 hex>, benchmark.py create_benchmark_run, run.py create_scan_run) -> <RUN_ID>",
|
|
"trace/events.jsonl as a whole (timings); never copied",
|
|
"the export's absolute root path -> <ROOT>; the children's TMPDIR -> <TMP>",
|
|
"suite: only the outcome per test id is kept (junit durations dropped)",
|
|
"env.json is a provenance record (commit, freeze hash, instrument checks), not compared",
|
|
]
|
|
|
|
INPUTS = (
|
|
"benchmarks/pairs/catalog.toml",
|
|
"benchmarks/manifests/python-vulnerable.toml",
|
|
"benchmarks/manifests/java-spring-boot-vulnerable.toml",
|
|
)
|
|
SCAN_KEEP = ("findings.json", "verification.json", "fusion.json", "report.sarif", "harness.json", "manifest.json")
|
|
BENCH_KEEP = ("benchmark_result.json", "calibration_records.json", "external_baseline_deltas.json")
|
|
|
|
|
|
class InstrumentError(SystemExit):
|
|
def __init__(self, message: str) -> None:
|
|
super().__init__(f"instrument check failed: {message}")
|
|
|
|
|
|
def normalize(value: Any, root: str, tmp: str) -> Any:
|
|
"""Drop excluded keys recursively and scrub the export root, TMPDIR and run ids from strings."""
|
|
if isinstance(value, dict):
|
|
return {k: normalize(v, root, tmp) for k, v in value.items() if not _excluded(k)}
|
|
if isinstance(value, list):
|
|
return [normalize(v, root, tmp) for v in value]
|
|
if isinstance(value, str):
|
|
return scrub(value, root, tmp)
|
|
return value
|
|
|
|
|
|
def _excluded(key: str) -> bool:
|
|
return key in EXCLUDED_KEYS or key.endswith(EXCLUDED_SUFFIXES)
|
|
|
|
|
|
def scrub(text: str, root: str, tmp: str) -> str:
|
|
text = text.replace(tmp, "<TMP>").replace(root, "<ROOT>")
|
|
return RUN_ID.sub("<RUN_ID>", text)
|
|
|
|
|
|
def dump(value: Any) -> str:
|
|
return json.dumps(value, indent=1, sort_keys=True) + "\n"
|
|
|
|
|
|
def _child_env(root: Path, tmp: Path) -> dict[str, str]:
|
|
env = {k: v for k, v in os.environ.items() if not k.endswith("_API_KEY") and k not in {"PYTHONPATH", "VIRTUAL_ENV"}}
|
|
env.update(OPENULTRASAST_SKIP_DOTENV="1", PYTHONPATH=str(root / "src"), TMPDIR=str(tmp), PYTHONHASHSEED="0")
|
|
return env
|
|
|
|
|
|
def _run(cmd: list[str], root: Path, env: dict[str, str], label: str) -> str:
|
|
proc = subprocess.run(cmd, cwd=root, env=env, capture_output=True, text=True, check=False)
|
|
if proc.returncode != 0:
|
|
tail = (proc.stdout + proc.stderr)[-2000:]
|
|
raise InstrumentError(f"{label} exited {proc.returncode}: {' '.join(cmd)}\n{tail}")
|
|
return proc.stdout
|
|
|
|
|
|
def _field(stdout: str, name: str) -> str:
|
|
for line in stdout.splitlines():
|
|
if line.startswith(f"{name}="):
|
|
return line.partition("=")[2]
|
|
raise InstrumentError(f"no {name}= line in output")
|
|
|
|
|
|
def _freeze_hash() -> str:
|
|
uv = shutil.which("uv")
|
|
if uv is None:
|
|
raise InstrumentError("uv not found; cannot hash the environment")
|
|
out = subprocess.run([uv, "pip", "freeze", "--python", sys.executable], capture_output=True, text=True, check=True).stdout
|
|
return hashlib.sha256(out.encode()).hexdigest()
|
|
|
|
|
|
def _suite(xml_path: Path) -> dict[str, str]:
|
|
outcomes: dict[str, str] = {}
|
|
for case in ET.parse(xml_path).getroot().iter("testcase"):
|
|
test_id = f"{case.get('classname')}::{case.get('name')}"
|
|
kinds = {child.tag for child in case}
|
|
outcome = next((k for k in ("error", "failure", "skipped") if k in kinds), "passed")
|
|
outcomes[test_id] = outcome
|
|
return dict(sorted(outcomes.items()))
|
|
|
|
|
|
def record(out: Path, tree_ish: str, workdir: Path) -> None:
|
|
if importlib.util.find_spec("harnessx") is not None:
|
|
raise InstrumentError("harnessx is importable; re-sync the venv without the extra")
|
|
if importlib.util.find_spec("tree_sitter") is None:
|
|
raise InstrumentError("tree_sitter is not importable; the semantic extra is missing")
|
|
workdir.mkdir(parents=True, exist_ok=True)
|
|
root = Path(tempfile.mkdtemp(prefix="export-", dir=workdir)).resolve()
|
|
tmp = Path(tempfile.mkdtemp(prefix="tmp-", dir=workdir)).resolve()
|
|
archive = subprocess.run(["git", "-C", str(REPO), "archive", tree_ish], capture_output=True, check=True).stdout
|
|
subprocess.run(["tar", "-x", "-C", str(root)], input=archive, check=True)
|
|
env = _child_env(root, tmp)
|
|
rs, ts = str(root), str(tmp)
|
|
py = sys.executable
|
|
out.mkdir(parents=True, exist_ok=True)
|
|
checks: dict[str, Any] = {}
|
|
|
|
module = _run([py, "-c", "import openultrasast; print(openultrasast.__file__)"], root, env, "import probe").strip()
|
|
if not module.startswith(rs):
|
|
raise InstrumentError(f"openultrasast imported from {module}, not the export {rs}")
|
|
checks["openultrasast_module"] = scrub(module, rs, ts)
|
|
inputs = {name: (root / name).stat().st_size for name in INPUTS}
|
|
if not all(inputs.values()):
|
|
raise InstrumentError(f"empty input manifest: {inputs}")
|
|
print(f"export={root} module={module} inputs={inputs}", flush=True)
|
|
|
|
# 1. Full suite, outcome per test id.
|
|
junit = tmp / "suite.xml"
|
|
_run([py, "-m", "pytest", "-q", "-p", "no:cacheprovider", f"--junitxml={junit}"], root, env, "pytest")
|
|
suite = _suite(junit)
|
|
counts = {k: sum(1 for v in suite.values() if v == k) for k in ("passed", "skipped", "failure", "error")}
|
|
if counts["passed"] == 0:
|
|
raise InstrumentError("suite recorded no passing tests")
|
|
(out / "suite.json").write_text(dump(suite))
|
|
checks["suite"] = counts
|
|
print(f"suite={counts}", flush=True)
|
|
|
|
# 2. Pair replay (offline, vendored pairs only).
|
|
pairs = normalize(json.loads(_run([py, "-m", "openultrasast.cli", "pairs", "--json"], root, env, "pairs")), rs, ts)
|
|
checks["pairs"] = _pair_counts(pairs)
|
|
(out / "pairs.json").write_text(dump(pairs))
|
|
print(f"pairs={checks['pairs']}", flush=True)
|
|
|
|
# 3. Quick benchmark replay.
|
|
empty = tmp / "empty.toml"
|
|
empty.write_text("")
|
|
cmd = [
|
|
py,
|
|
"-m",
|
|
"openultrasast.cli",
|
|
"benchmark",
|
|
"benchmarks/manifests/python-vulnerable.toml",
|
|
"--mode",
|
|
"quick",
|
|
"--config",
|
|
str(empty),
|
|
]
|
|
stdout = _run(cmd, root, env, "benchmark")
|
|
expected = int(_field(stdout, "benchmark_expected"))
|
|
if expected <= 0:
|
|
raise InstrumentError("benchmark expected no findings")
|
|
checks["benchmark"] = {k: int(_field(stdout, f"benchmark_{k}")) for k in ("expected", "matched", "missed")}
|
|
checks["benchmark"]["findings"] = int(_field(stdout, "findings"))
|
|
_keep(Path(_field(stdout, "benchmark_run_dir")), BENCH_KEEP, out / "benchmark", rs, ts, checks, "benchmark_files")
|
|
(out / "benchmark" / "stdout.txt").write_text(scrub(stdout, rs, ts))
|
|
print(f"benchmark={checks['benchmark']}", flush=True)
|
|
|
|
# 4. One standard-mode scan (verify + fusion, no model configured).
|
|
cmd = [py, "-m", "openultrasast.cli", "scan", "benchmarks/fixtures/python-vulnerable", "--mode", "standard", "--config", str(empty)]
|
|
stdout = _run(cmd, root, env, "scan")
|
|
checks["scan"] = {"findings": int(_field(stdout, "findings")), "file_targets": int(_field(stdout, "file_targets"))}
|
|
if checks["scan"]["file_targets"] <= 0:
|
|
raise InstrumentError("standard scan saw no file targets")
|
|
_keep(Path(_field(stdout, "run_dir")), SCAN_KEEP, out / "scan", rs, ts, checks, "scan_files")
|
|
(out / "scan" / "stdout.txt").write_text(scrub(stdout, rs, ts))
|
|
print(f"scan={checks['scan']}", flush=True)
|
|
|
|
# 5. improve --dry-run; the shim keeps the dry-run journal the CLI writes to a throwaway directory.
|
|
shim_out = tmp / "improve"
|
|
cmd = [py, str(Path(__file__).resolve()), "_improve", str(shim_out), "benchmarks/manifests/java-spring-boot-vulnerable.toml"]
|
|
stdout = _run(cmd, root, env, "improve")
|
|
(out / "improve").mkdir(exist_ok=True)
|
|
(out / "improve" / "stdout.txt").write_text(scrub(stdout, rs, ts))
|
|
# A round with no proposals writes no journal (evolve.py returns before append_round); its absence is
|
|
# recorded and compared like any file, so a journal appearing after the removal is a difference.
|
|
rounds = [line for line in stdout.splitlines() if line.startswith("round ")]
|
|
if not rounds or "rounds=" not in stdout:
|
|
raise InstrumentError("improve --dry-run printed no round")
|
|
journal = shim_out / "improve_journal.json"
|
|
checks["improve"] = {"rounds": len(rounds), "journal_bytes": journal.stat().st_size if journal.is_file() else None}
|
|
if journal.is_file():
|
|
journal_data = normalize(json.loads(journal.read_text()), rs, ts)
|
|
(out / "improve" / "improve_journal.json").write_text(dump(journal_data))
|
|
print(f"improve={checks['improve']}", flush=True)
|
|
|
|
head = subprocess.run(["git", "-C", str(REPO), "rev-parse", tree_ish], capture_output=True, text=True, check=True).stdout.strip()
|
|
env_record = {
|
|
"tree_ish": head,
|
|
"freeze_sha256": _freeze_hash(),
|
|
"harnessx_importable": False,
|
|
"tree_sitter": True,
|
|
"python": sys.version.split()[0],
|
|
"child_env": {"OPENULTRASAST_SKIP_DOTENV": "1", "PYTHONHASHSEED": "0", "api_keys": "every *_API_KEY unset", "config": "empty"},
|
|
"inputs": inputs,
|
|
"instrument_checks": checks,
|
|
"exclusions": EXCLUSIONS,
|
|
}
|
|
(out / "env.json").write_text(dump(env_record))
|
|
(out / "README").write_text("Excluded from comparison: " + "; ".join(EXCLUSIONS) + "\n")
|
|
shutil.rmtree(root, ignore_errors=True)
|
|
shutil.rmtree(tmp, ignore_errors=True)
|
|
print(f"recorded {out}")
|
|
|
|
|
|
def _pair_counts(pairs: Any) -> dict[str, int]:
|
|
overall = pairs.get("overall", {}) if isinstance(pairs, dict) else {}
|
|
counts = {k: int(overall.get(k, 0)) for k in ("pairs", "detected_vuln", "pair_correct", "labeled_expected", "labeled_matched")}
|
|
counts["outcomes"] = len(pairs.get("outcomes", [])) if isinstance(pairs, dict) else 0
|
|
if counts["pairs"] <= 0 or counts["detected_vuln"] <= 0 or counts["outcomes"] <= 0:
|
|
raise InstrumentError(f"pair replay saw {counts}")
|
|
return counts
|
|
|
|
|
|
def _keep(run_dir: Path, names: tuple[str, ...], dest: Path, rs: str, ts: str, checks: dict[str, Any], key: str) -> None:
|
|
if not run_dir.is_dir():
|
|
raise InstrumentError(f"run directory missing: {run_dir}")
|
|
dest.mkdir(parents=True, exist_ok=True)
|
|
present = {}
|
|
for name in names:
|
|
source = run_dir / name
|
|
if source.is_file():
|
|
present[name] = source.stat().st_size
|
|
(dest / name).write_text(dump(normalize(json.loads(source.read_text()), rs, ts)))
|
|
checks[key] = present
|
|
|
|
|
|
def _improve_shim(out: Path, manifest: str) -> int:
|
|
"""Run ``ousast improve --dry-run --no-pair-gate`` and copy the throwaway journal out before cleanup."""
|
|
from openultrasast import cli
|
|
|
|
original = tempfile.TemporaryDirectory
|
|
|
|
class Keeping(original): # type: ignore[misc,valid-type]
|
|
def cleanup(self) -> None:
|
|
out.mkdir(parents=True, exist_ok=True)
|
|
for name in ("improve_journal.json",):
|
|
if (Path(self.name) / name).is_file():
|
|
shutil.copy2(Path(self.name) / name, out / name)
|
|
super().cleanup()
|
|
|
|
def __exit__(self, *exc: object) -> None:
|
|
self.cleanup()
|
|
|
|
cli.tempfile.TemporaryDirectory = Keeping # type: ignore[misc]
|
|
try:
|
|
return int(cli.main(["improve", manifest, "--dry-run", "--no-pair-gate"]) or 0)
|
|
finally:
|
|
cli.tempfile.TemporaryDirectory = original # type: ignore[misc]
|
|
|
|
|
|
def compare(baseline: Path, candidate: Path, suite_map: dict[str, Any]) -> list[str]:
|
|
"""Return every difference outside the exclusion list; empty means equal."""
|
|
diffs: list[str] = []
|
|
diffs += _compare_suite(
|
|
json.loads((baseline / "suite.json").read_text()), json.loads((candidate / "suite.json").read_text()), suite_map
|
|
)
|
|
files = sorted(
|
|
{p.relative_to(baseline).as_posix() for p in baseline.rglob("*") if p.is_file()}
|
|
| {p.relative_to(candidate).as_posix() for p in candidate.rglob("*") if p.is_file()}
|
|
)
|
|
for name in files:
|
|
if name in {"suite.json", "env.json", "README", "comparison.json", "determinism.json"}:
|
|
continue
|
|
a, b = baseline / name, candidate / name
|
|
if not a.is_file() or not b.is_file():
|
|
diffs.append(f"{name}: present only in {'candidate' if b.is_file() else 'baseline'}")
|
|
continue
|
|
if a.read_bytes() == b.read_bytes():
|
|
continue
|
|
if name.endswith((".json", ".sarif")):
|
|
diffs.append(f"{name}: {_first_json_diff(json.loads(a.read_text()), json.loads(b.read_text()), '$')}")
|
|
else:
|
|
diffs.append(f"{name}: first differing line {_first_line_diff(a.read_text(), b.read_text())}")
|
|
return diffs
|
|
|
|
|
|
def _compare_suite(a: dict[str, str], b: dict[str, str], suite_map: dict[str, Any]) -> list[str]:
|
|
deleted = set(suite_map.get("deleted", ()))
|
|
renamed: dict[str, str] = dict(suite_map.get("renamed", {}))
|
|
added = set(suite_map.get("added", ())) | set(renamed.values())
|
|
diffs = []
|
|
for test_id, outcome in a.items():
|
|
if test_id in deleted:
|
|
continue
|
|
target = renamed.get(test_id, test_id)
|
|
if target not in b:
|
|
diffs.append(f"suite.json: {test_id} missing from candidate")
|
|
elif b[target] != outcome:
|
|
diffs.append(f"suite.json: {test_id} {outcome} -> {b[target]}")
|
|
diffs += [f"suite.json: {t} new in candidate" for t in b if t not in a and t not in added]
|
|
return diffs
|
|
|
|
|
|
def _first_json_diff(a: Any, b: Any, path: str) -> str:
|
|
if type(a) is not type(b):
|
|
return f"{path}: type {type(a).__name__} -> {type(b).__name__}"
|
|
if isinstance(a, dict):
|
|
for key in sorted(set(a) | set(b)):
|
|
if key not in a or key not in b:
|
|
return f"{path}.{key}: present only in {'candidate' if key in b else 'baseline'}"
|
|
if a[key] != b[key]:
|
|
return _first_json_diff(a[key], b[key], f"{path}.{key}")
|
|
if isinstance(a, list):
|
|
if len(a) != len(b):
|
|
return f"{path}: length {len(a)} -> {len(b)}"
|
|
for i, (x, y) in enumerate(zip(a, b, strict=True)):
|
|
if x != y:
|
|
return _first_json_diff(x, y, f"{path}[{i}]")
|
|
return f"{path}: {json.dumps(a)[:120]} -> {json.dumps(b)[:120]}"
|
|
|
|
|
|
def _first_line_diff(a: str, b: str) -> str:
|
|
la, lb = a.splitlines(), b.splitlines()
|
|
for i, (x, y) in enumerate(zip(la, lb, strict=False), 1):
|
|
if x != y:
|
|
return f"{i}: {x[:120]!r} -> {y[:120]!r}"
|
|
return f"line count {len(la)} -> {len(lb)}"
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
sub = parser.add_subparsers(dest="command", required=True)
|
|
rec = sub.add_parser("record")
|
|
rec.add_argument("out", type=Path)
|
|
rec.add_argument("--tree-ish", default="HEAD", help="git tree-ish to export (a commit, or `git write-tree` for the index)")
|
|
rec.add_argument("--workdir", type=Path, default=Path(tempfile.gettempdir()) / "hxr-equality")
|
|
cmp_ = sub.add_parser("compare")
|
|
cmp_.add_argument("baseline", type=Path)
|
|
cmp_.add_argument("candidate", type=Path)
|
|
cmp_.add_argument("--suite-map", type=Path, help='JSON {"deleted": [...], "renamed": {old: new}, "added": [...]}')
|
|
cmp_.add_argument("--record", type=Path, help="write the comparison result here as JSON")
|
|
shim = sub.add_parser("_improve")
|
|
shim.add_argument("out", type=Path)
|
|
shim.add_argument("manifest")
|
|
args = parser.parse_args(argv)
|
|
if args.command == "record":
|
|
record(args.out.resolve(), args.tree_ish, args.workdir.resolve())
|
|
return 0
|
|
if args.command == "_improve":
|
|
return _improve_shim(args.out, args.manifest)
|
|
suite_map = json.loads(args.suite_map.read_text()) if args.suite_map else {}
|
|
diffs = compare(args.baseline, args.candidate, suite_map)
|
|
if args.record:
|
|
args.record.write_text(
|
|
dump(
|
|
{
|
|
"baseline": str(args.baseline),
|
|
"candidate": str(args.candidate),
|
|
"equal": not diffs,
|
|
"differences": diffs,
|
|
"exclusions": EXCLUSIONS,
|
|
}
|
|
)
|
|
)
|
|
for line in diffs[:20]:
|
|
print(line)
|
|
print(f"equal={not diffs} differences={len(diffs)}")
|
|
return 1 if diffs else 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|