mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-16 03:49:26 +00:00
DSPy baseline: 2/5 pushes, 4/48 locations, 14 of 90 ordinary pushes flagged, 25 suspicions (3 true), 11 invalid-request failures in shopper and trigger.dev, $6.13. Pilot hunter: 2/5 pushes, 4/48 locations, 99 of 100 ordinary pushes flagged, 594 findings (3 true). Different pushes caught; nothing in trigger.dev. The runner booked usd 0.0 per change so --budget-usd never stopped a run; it now prices every LM call of the change at the registered prices and writes usd into the record. Co-Authored-By: Claude Fable 5.1 <[email protected]>
144 lines
6.3 KiB
Python
144 lines
6.3 KiB
Python
"""Both arms over the frozen access-control training pool (one run each): push recall, location recall, false alarms.
|
|
|
|
Unit = one push (change). A push is caught when any verified location of its fix is caught
|
|
(`benchmarks.unseen.score.catches` on the per-target view, as `benchmarks/valid/metrics.py` does). A location is
|
|
caught when its own view is caught. Instrument failures and unparsed hunts stay in the denominators as misses,
|
|
and false alarms are counted over the ordinary pushes the arm actually scanned (instrument failures excluded, and
|
|
said so). Every suspicion is also written out with a true/false label for feature work.
|
|
|
|
PYTHONPATH=. .venv/bin/python benchmarks/measurements/2026-10-14-access-control-hunt/analyse.py ~/ousast-results/hunt/ac-train
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import collections
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
from benchmarks.unseen import score
|
|
from openultrasast import stats
|
|
from openultrasast.model.endpoint import _PRICES as PRICES
|
|
|
|
|
|
def targets_of(record: dict) -> list[dict]:
|
|
if record["kind"] != "vulnerability":
|
|
return []
|
|
return record.get("targets") or [
|
|
{"case_id": record["case_id"], "path": record["path"], "function": record["function"], "function_lines": record["function_lines"]}
|
|
]
|
|
|
|
|
|
def view(record: dict, target: dict) -> dict:
|
|
return {**record, **target, "files": [{"path": target["path"], "head_lines": [target["function_lines"]]}]}
|
|
|
|
|
|
def defects(record: dict) -> list[dict]:
|
|
return record.get("push", {}).get("admission", {}).get("defects", [])
|
|
|
|
|
|
def arm_summary(records: list[dict], arm: str) -> tuple[dict, list[dict]]:
|
|
usable = [r for r in records if score.coverage(r) != "instrument_failure"]
|
|
positives = [r for r in records if r["kind"] == "vulnerability"]
|
|
ordinary = [r for r in usable if r["kind"] == "ordinary"]
|
|
push_hits = [int(any(score.catches(view(r, t)) for t in targets_of(r))) for r in positives]
|
|
locations = [(r["repo"], int(bool(score.catches(view(r, t))))) for r in positives for t in targets_of(r)]
|
|
flagged = [r for r in ordinary if score.false_alarm(r)]
|
|
suspicions = []
|
|
for r in records:
|
|
for d in defects(r):
|
|
single = {**r, "push": {"admission": {"defects": [d], "dispositions": []}, "quick_tier": []}}
|
|
hit = any(score.catches(view(single, t)) for t in targets_of(r))
|
|
confidence = d.get("confidence") if arm == "dspy" else (d.get("classifier") or {}).get("confidence")
|
|
location = (d.get("locations") or [[None, None]])[0]
|
|
suspicions.append(
|
|
{
|
|
"arm": arm,
|
|
"repo": r["repo"],
|
|
"change_id": r["id"],
|
|
"kind": r["kind"],
|
|
"path": location[0],
|
|
"line": location[1] if len(location) > 1 else None,
|
|
"family": d.get("family"),
|
|
"confidence": confidence,
|
|
"label": int(hit),
|
|
}
|
|
)
|
|
true_suspicions = sum(s["label"] for s in suspicions)
|
|
summary = {
|
|
"pushes": len(records),
|
|
"instrument_failures": {
|
|
"count": len(records) - len(usable),
|
|
"types": dict(
|
|
collections.Counter(r["instrument"].get("failure") for r in records if score.coverage(r) == "instrument_failure")
|
|
),
|
|
},
|
|
"parse_failures": sum(bool(r.get("parse_failed")) for r in records),
|
|
"loop_parse_failures": sum(bool(r.get("loop_parse_failed")) for r in records),
|
|
"push_recall": {"hits": sum(push_hits), "n": len(push_hits), "wilson95": stats.wilson(sum(push_hits), len(push_hits))},
|
|
"location_recall": {
|
|
"hits": sum(h for _, h in locations),
|
|
"n": len(locations),
|
|
"wilson95": stats.wilson(sum(h for _, h in locations), len(locations)),
|
|
"per_repository": {
|
|
repo: f"{sum(h for rp, h in locations if rp == repo)}/{sum(1 for rp, _ in locations if rp == repo)}"
|
|
for repo in sorted({rp for rp, _ in locations})
|
|
},
|
|
},
|
|
"false_alarms": {
|
|
"flagged_ordinary_pushes": len(flagged),
|
|
"ordinary_pushes_scanned": len(ordinary),
|
|
"per_100_pushes": 100 * len(flagged) / len(ordinary) if ordinary else None,
|
|
"wilson95_per_100": [100 * v for v in stats.wilson(len(flagged), len(ordinary))] if ordinary else None,
|
|
},
|
|
"suspicions": {
|
|
"total": len(suspicions),
|
|
"true": true_suspicions,
|
|
"precision": true_suspicions / len(suspicions) if suspicions else None,
|
|
"by_family": dict(collections.Counter(s["family"] for s in suspicions)),
|
|
"true_by_family": dict(collections.Counter(s["family"] for s in suspicions if s["label"])),
|
|
},
|
|
}
|
|
return summary, suspicions
|
|
|
|
|
|
def cost(log: Path) -> dict | None:
|
|
for line in log.read_text().splitlines():
|
|
if '"tokens"' not in line:
|
|
continue
|
|
tokens = json.loads(line)["tokens"]
|
|
out = {}
|
|
for model, usage in tokens.items():
|
|
prices = PRICES.get(model.split("/")[-1])
|
|
cached = usage.get("prompt_tokens_details", {}).get("cached_tokens", 0)
|
|
uncached = usage["prompt_tokens"] - cached
|
|
usd = None
|
|
if prices:
|
|
usd = (
|
|
uncached * prices.input_per_m + cached * prices.cache_hit_per_m + usage["completion_tokens"] * prices.output_per_m
|
|
) / 1e6
|
|
out[model] = {
|
|
"prompt_tokens": usage["prompt_tokens"],
|
|
"cached_tokens": cached,
|
|
"completion_tokens": usage["completion_tokens"],
|
|
"usd": usd,
|
|
}
|
|
return out
|
|
return None
|
|
|
|
|
|
def main(root: Path) -> dict:
|
|
out: dict = {}
|
|
all_suspicions = []
|
|
for arm, folder in (("dspy", "dspy-1"), ("hunter", "hunter-1")):
|
|
records = score.load_records(root / folder)
|
|
out[arm], suspicions = arm_summary(records, arm)
|
|
all_suspicions.extend(suspicions)
|
|
out["dspy"]["cost"] = cost(root / "dspy-1.log")
|
|
(root / "suspicions.jsonl").write_text("".join(json.dumps(s) + "\n" for s in all_suspicions))
|
|
return out
|
|
|
|
|
|
if __name__ == "__main__":
|
|
print(json.dumps(main(Path(sys.argv[1]).expanduser()), indent=2, default=str))
|