"""Resumable, serial labelled-function traces; host fetches pinned sources, Docker stays offline. Pair blob identities resolve through catalogs to real source commits. The analyzer is git archive HEAD src plus a hashed snapshot of this uncommitted tool. """ from __future__ import annotations import argparse import fcntl import hashlib import json import os import re import shutil import subprocess import tempfile import time import tomllib from collections import Counter, defaultdict from pathlib import Path from engine_trace_worker import EXCLUDED_DIRS, safe_path, unit_record, write_json from openultrasast.learn.examples import read_jsonl from openultrasast.learn.labels import load_sources, repo_name from openultrasast.model.specs import taint_specs from openultrasast.model.taint import request_params from openultrasast.pairs import _materialize_side, load_pair_catalog from openultrasast.plane.engine_alerts import export_pin from openultrasast.plane.memory import FileStore from openultrasast.plane.tasks.alerts import engine_languages from openultrasast.preprocess import detect_language ROOT = Path(__file__).resolve().parents[2] DEFAULT_OUT = Path.home() / "ousast-results/plane/engine-trace" SNAPSHOT_FILES = ( "benchmarks/learn/engine_trace.py", "benchmarks/learn/engine_trace_worker.py", "src/openultrasast/model/trace.py", "src/openultrasast/model/taint.py", "src/openultrasast/cpg/queries/taint.sc", ) MATERIALIZATION_VERSION = 2 EXPORT_CAP = 20 * 1024 * 1024 STATUSES = ("path", "asked-nothing", "failed", "timeout", "unsupported") def digest(value): return hashlib.sha256(json.dumps(value, sort_keys=True).encode()).hexdigest() def pin_name(pin): return digest([pin["repo"], pin["pin"]]) + ".json" def join_units(units, examples, include_typescript=False): index = {row["id"]: row for row in examples} missing = [u["unit"] for u in units if u["unit"] not in index] if missing: raise ValueError(f"example snapshot missing {len(missing)}/{len(units)} unit IDs; supply --examples JSONL; first: {missing[0]}") covered = engine_languages() - {"typescript"} if include_typescript: covered |= {"typescript"} result = [] for unit in units: example = index[unit["unit"]] if any(example[key] != unit[key] for key in ("family", "label", "group")): raise ValueError(f"unit/example identity mismatch: {unit['unit']}") file, sep, function = example["candidate"].partition("::") if not sep or not function: raise ValueError(f"unit has no labelled function: {unit['unit']}") language = detect_language(Path(file)) or example.get("language", "other") model_language = "javascript" if language == "typescript" else language spec = taint_specs(language=model_language).get(unit["family"]) if language in covered else None result.append( { **unit, "repo": example["repo"], "pin": example["pin"], "file": file, "function": function, "language": language, "source": example["source"], "excerpt": example.get("excerpt", example["source"] == "pairs" or example["source"].startswith("advisory-fixes")), "pin_role": example.get("pin_role", ""), "supported": spec is not None, "reason": "" if spec else f"no admitted taint model: {language}/{unit['family']}", } ) return result def make_plan( units, out, only_family=None, limit=0, *, include_typescript=False, only_source=(), rerun_status=("failed",), allow_excerpt_fallback=False, ): grouped = defaultdict(list) for unit in units: if (not only_family or unit["family"] == only_family) and (not only_source or unit["source"] in only_source): # Include admission options even when a unit's support happens to stay # unchanged. Old results cannot mark a different selection as complete. identity = { "unit": unit, "include_typescript": include_typescript, "materialization_version": MATERIALIZATION_VERSION, "allow_excerpt_fallback": allow_excerpt_fallback, } grouped[unit["repo"], unit["pin"]].append({**unit, "input_digest": digest(identity)}) plan = [] for (repo, pin), rows in sorted(grouped.items()): item = { "repo": repo, "pin": pin, "materialization_version": MATERIALIZATION_VERSION, "units": sorted(rows, key=lambda r: r["unit"]), } item["selection"] = digest(item["units"]) done = out / pin_name(item) if done.exists(): old = json.loads(done.read_text()) recorded = {r["unit"]: r.get("input_digest") for r in old.get("units", [])} if ( old.get("done") and old.get("materialization_version") == MATERIALIZATION_VERSION and not any(r.get("status") in rerun_status for r in old.get("units", []) if r["unit"] in {u["unit"] for u in rows}) and all(recorded.get(r["unit"]) == r["input_digest"] for r in rows) ): continue sources = {r["source"] for r in rows} # Inventory medians: harvest sides 31s, advisory sides 47s; v1 pins 182s, v2 pins 498s. item["estimated_seconds"] = ( (47 if any(s.startswith("advisory") for s in sources) else 31) if any(r.get("excerpt") for r in rows) or any(s.startswith("advisory") for s in sources) else (182 if sources == {"population-v1"} else 498) ) if not any(r["supported"] for r in rows): item["estimated_seconds"] = 0 plan.append(item) # limit counts runnable pins, not unsupported records. if limit: chosen = [] count = 0 for pin in plan: runnable = any(r["supported"] for r in pin["units"]) if runnable and count >= limit: continue chosen.append(pin) count += runnable return chosen return plan class Inputs: """Resolve spent corpora; fetch pair source commits into the host cache.""" def __init__(self, root, cache, allow_excerpt_fallback=False): self.cache = cache self.allow_excerpt_fallback = allow_excerpt_fallback self.catalog_rows = defaultdict(list) for catalog in (root / "benchmarks/pairs").rglob("catalog.toml"): for item in tomllib.loads(catalog.read_text()).get("pair", []): self.catalog_rows[item["name"]].append((catalog.parent.name, item)) self.pairs = [] self.repos = defaultdict(list) sources = load_sources() for source in sources.sources: if source.kind == "pairs": self.pairs.extend(load_pair_catalog(root / source.files[0])) elif source.kind == "population": if source.id not in {"population-v1", "population-v2"}: continue data = tomllib.loads((root / source.files[0]).read_text()) for case in data.get("case", []): self.repos[repo_name(case["repo"])].append((cache / "independent" / case["id"], case)) elif source.kind == "recipes": for file in source.files: data = tomllib.loads((root / file).read_text()) recipe = data.get("repo", data) # Recipes carry a top-level URL and a pinned checkout cache. if isinstance(recipe, dict): self.repos[repo_name(str(recipe.get("url", "")))].append((cache / "repos" / str(data.get("name", "")), data)) self.hashes = {} def pair_for(self, row): if row["pin_role"] not in {"fixed", "vulnerable", "vuln"}: raise ValueError(f"ambiguous pair side: {row['pin_role']}") side = "fixed" if row["pin_role"] == "fixed" else "vuln" candidates = [ c for c in self.pairs if c.relpath == row["file"] and repo_name(c.repo or f"{c.slice}:{c.name}") == repo_name(row["repo"]) ] matches = [] for case in candidates: path = case.fixed_file if side == "fixed" else case.vuln_file if path.is_file(): if path not in self.hashes: data = path.read_bytes() # Harvest hashes decoded text, so universal newline conversion is part # of its identity (one recorded ThreatByte excerpt uses CRLF). normalized = path.read_text().encode("utf-8") self.hashes[path] = { hashlib.sha1(b"blob " + str(len(body)).encode() + b"\0" + body).hexdigest() for body in (data, normalized) } if row["pin"] in self.hashes[path]: matches.append(case) if len(matches) == 1: return matches[0], side if len(candidates) == 1: # Earlier stores used label_pin rather than the excerpt digest. from openultrasast.learn.examples import label_pin case = candidates[0] if row["pin"] == label_pin({"source": "pairs", "source_ref": case.name, "pin_role": row["pin_role"]}): return case, side raise ValueError(f"cannot uniquely resolve excerpt {row['repo']}@{row['pin']}:{row['file']}") def catalog_for(self, case): matches = [ (source, item) for source, item in self.catalog_rows[case.name] if item.get("relpath") == case.relpath and repo_name(item.get("repo", "")) == repo_name(case.repo) ] if len(matches) != 1: raise ValueError(f"ambiguous catalog source for {case.name}") return matches[0] def source_identity(self, case, side): _, item = self.catalog_for(case) parent, commit = item.get("parent", ""), item.get("commit", "") if side not in {"vuln", "fixed"} or not all(re.fullmatch(r"[0-9a-fA-F]{40}", sha) for sha in (parent, commit)) or parent == commit: raise ValueError(f"ambiguous catalog commits for {case.name}: need distinct parent and commit SHAs") repo = item.get("repo", "") if not repo: raise ValueError(f"missing catalog repository for {case.name}") return repo, parent if side == "vuln" else commit def fetch_source(self, repo, commit, roots): clone = self.cache / "trace-sources" / digest([repo, commit, roots]) clone.mkdir(parents=True, exist_ok=True) url = repo if repo.startswith("https://") else f"https://github.com/{repo.removesuffix('.git')}.git" env = {**os.environ, "GIT_TERMINAL_PROMPT": "0"} env.pop("GIT_NO_LAZY_FETCH", None) # sparse checkout may fetch the selected blobs def git(*args): return subprocess.run(["git", "-C", str(clone), *args], env=env, capture_output=True, text=True, check=True, timeout=300) git("init") git("config", "remote.origin.url", url) git("config", "remote.origin.promisor", "true") git("config", "remote.origin.partialclonefilter", "blob:none") git("fetch", "--depth", "1", "--filter=blob:none", url, commit) git("sparse-checkout", "init", "--cone") git("sparse-checkout", "set", "--", *roots) git("checkout", "--detach", "--force", commit) actual = git("rev-parse", "HEAD").stdout.strip() if actual != commit: raise ValueError(f"fetched commit mismatch: {actual} != {commit}") return clone def export_local(self, clone, pin, target): with tempfile.TemporaryDirectory(prefix="trace-export-", dir=target.parent) as scratch: export_pin(clone, pin["pin"], Path(scratch)) proof = export_sources(Path(scratch), target, pin["units"]) return {**proof, "mode": "source", "commit": pin["pin"]} def materialize(self, pin, target): rows = pin["units"] for row in rows: safe_path(target, row["file"]) if row.get("mapping_error"): raise ValueError(row["mapping_error"]) if any(r.get("excerpt") or r["source"].startswith("advisory-fixes") for r in rows): resolved = [self.pair_for(row) for row in rows] identities = {self.source_identity(case, side) for case, side in resolved} if len(identities) != 1: raise ValueError("ambiguous source commits at one pair pin") repo, commit = identities.pop() roots = sorted({source_root(row["file"], row["language"]) for row in rows}) try: clone = self.fetch_source(repo, commit, roots) proof = export_sources(clone, target, rows, roots) except (OSError, ValueError, subprocess.SubprocessError) as exc: if not self.allow_excerpt_fallback: raise shutil.rmtree(target, ignore_errors=True) for case, side in resolved: _materialize_side(target, case, side=side) return {"mode": "excerpt-fallback", "reason": str(exc), "repo": repo, "commit": commit} verify_functions(target, rows) return {**proof, "mode": "source", "repo": repo, "commit": commit, "roots": roots} if all(r["source"] == "dev-php" for r in rows): # Export the exact requested commit from the local recipe checkout. for recipe in (ROOT / "benchmarks/repos").glob("*.toml"): data = tomllib.loads(recipe.read_text()) if repo_name(str(data.get("url", ""))) == repo_name(pin["repo"]): checkout = self.cache / "repos" / data["name"] / pin["pin"][:12] return self.export_local(checkout, pin, target) for clone, _ in self.repos[repo_name(pin["repo"])]: check = subprocess.run(["git", "-C", str(clone), "cat-file", "-e", pin["pin"] + "^{commit}"], capture_output=True) if check.returncode == 0: return self.export_local(clone, pin, target) raise ValueError(f"no local clone contains {pin['repo']}@{pin['pin']}") def source_root(file, language): parts = Path(file).parts if language == "java": for i in range(len(parts) - 2): if parts[i : i + 3] == ("src", "main", "java"): return "/".join(parts[: i + 3]) return parts[0] if len(parts) > 1 else "." def export_sources(source, target, rows, roots=(".",), cap=EXPORT_CAP): """Preserve labelled paths first; cap context and exclude non-source trees.""" labelled = {r["file"] for r in rows} languages = {r["language"] for r in rows} files = set() for directory, dirs, names in os.walk(source): dirs[:] = sorted(d for d in dirs if d not in EXCLUDED_DIRS and d != ".git" and not (Path(directory) / d).is_symlink()) for name in names: file = Path(directory) / name rel = file.relative_to(source).as_posix() if ( not file.is_symlink() and detect_language(file) in languages and any(root == "." or rel.startswith(root + "/") for root in roots) ): files.add(rel) missing = labelled - files if missing: raise ValueError(f"labelled files missing or excluded from export: {sorted(missing)}") proof = {"bytes": 0, "files": 0, "cap_bytes": cap, "cap_hit": False, "excludes": sorted(EXCLUDED_DIRS)} for rel in sorted(files, key=lambda rel: (rel not in labelled, rel)): file = safe_path(source, rel) size = file.stat().st_size if proof["bytes"] + size > cap: proof["cap_hit"] = True if rel in labelled: raise ValueError("labelled source exceeds 20 MB export cap") continue dest = safe_path(target, rel) dest.parent.mkdir(parents=True, exist_ok=True) dest.write_bytes(file.read_bytes()) proof["bytes"] += size proof["files"] += 1 return proof def verify_functions(root, rows): for row in rows: data = safe_path(root, row["file"]).read_text(errors="replace") if not re.search(r"(?