mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-16 19:59:26 +00:00
117 lines
5.4 KiB
Python
117 lines
5.4 KiB
Python
"""Present valid-benchmark metrics on top of the shipped unseen scorer."""
|
|||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import argparse
|
||
|
|
import json
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
from benchmarks.unseen import score
|
||
|
|
|
||
|
|
|
||
|
|
def demonstrated(proof: dict) -> bool:
|
||
|
|
"""Read search.verify.VerificationRecord, with affected then safe sides."""
|
||
|
|
verification = proof.get("verification", proof)
|
||
|
|
sides = verification.get("sides", [])
|
||
|
|
if verification.get("outcome") != "demonstrated" or len(sides) != 2:
|
||
|
|
return False
|
||
|
|
if any(side.get("outcome") != "observed" or len(side.get("runs", [])) != 3 for side in sides):
|
||
|
|
return False
|
||
|
|
return all(run.get("observed") is True for run in sides[0]["runs"]) and all(run.get("observed") is False for run in sides[1]["runs"])
|
||
|
|
|
||
|
|
|
||
|
|
def scoring_records(records: list[dict]) -> list[dict]:
|
||
|
|
"""Project a multi-function fix onto a caught target while keeping one push."""
|
||
|
|
projected = []
|
||
|
|
for record in records:
|
||
|
|
targets = record.get("targets", [])
|
||
|
|
if not targets:
|
||
|
|
projected.append(record)
|
||
|
|
continue
|
||
|
|
views = [{**record, **target, "files": [{"path": target["path"], "head_lines": [target["function_lines"]]}]} for target in targets]
|
||
|
|
projected.append(next((view for view in views if score.catches(view)), views[0]))
|
||
|
|
return projected
|
||
|
|
|
||
|
|
|
||
|
|
def summarize(records: list[dict], *, scored: dict | None = None, proofs: list[dict] = (), seed: int = 0) -> dict:
|
||
|
|
projected = scoring_records(records)
|
||
|
|
# Legacy score output uses only the primary target. Re-score grouped fixes
|
||
|
|
# through the same scorer so a catch at any verified site counts once.
|
||
|
|
if scored is None or any(row.get("targets") for row in records):
|
||
|
|
scored = score.score(projected, seed=seed)
|
||
|
|
if scored.get("total") != len(records):
|
||
|
|
raise ValueError("scored output and records have different counts")
|
||
|
|
if len({row["id"] for row in records}) != len(records):
|
||
|
|
raise ValueError("duplicate replay id")
|
||
|
|
usable = [row for row in projected if score.coverage(row) != "instrument_failure"]
|
||
|
|
positives = [row for row in usable if row["kind"] == "vulnerability"]
|
||
|
|
flagged = [row for row in usable if score.false_alarm(row)]
|
||
|
|
precision = score.rate([row["kind"] == "vulnerability" and bool(score.catches(row)) for row in flagged])
|
||
|
|
recall = {
|
||
|
|
**scored["catch"],
|
||
|
|
"cluster95": score.cluster_interval([(row["repo"], int(bool(score.catches(row)))) for row in positives], seed=seed),
|
||
|
|
}
|
||
|
|
false_alarms = {**scored["false_alarm"]}
|
||
|
|
false_alarms["rate"] = None if false_alarms["rate"] is None else false_alarms["rate"] * 100
|
||
|
|
for key in ("wilson95", "cluster95"):
|
||
|
|
false_alarms[key] = [None if value is None else value * 100 for value in false_alarms[key]]
|
||
|
|
joined = {}
|
||
|
|
for proof in proofs:
|
||
|
|
key = proof.get("case_id") or proof["id"]
|
||
|
|
if key in joined:
|
||
|
|
raise ValueError("duplicate proof case id")
|
||
|
|
joined[key] = proof
|
||
|
|
flagged_positives = [row for row in flagged if row["kind"] == "vulnerability"]
|
||
|
|
case_ids = [case_id for row in flagged_positives for case_id in (row.get("case_ids") or [row.get("case_id") or row["id"]])]
|
||
|
|
if len(set(case_ids)) != len(case_ids):
|
||
|
|
raise ValueError("duplicate flagged case id")
|
||
|
|
missing = sorted(key for key in case_ids if key not in joined)
|
||
|
|
exploit = {
|
||
|
|
**score.rate([demonstrated(joined[key]) for key in case_ids if key in joined]),
|
||
|
|
"flagged_positives": len(case_ids),
|
||
|
|
"flagged_positive_pushes": len(flagged_positives),
|
||
|
|
"not_yet_proved": missing,
|
||
|
|
"status": "not yet proved" if missing else "measured",
|
||
|
|
}
|
||
|
|
result = dict(recall=recall, precision=precision, false_alarms_per_100_pushes=false_alarms, exploit_verified=exploit)
|
||
|
|
result["headline"] = headline(result)
|
||
|
|
return result
|
||
|
|
|
||
|
|
|
||
|
|
def headline(result: dict) -> str:
|
||
|
|
def number(value, scale=1):
|
||
|
|
return "not measured" if value is None else f"{value * scale:.1f}"
|
||
|
|
|
||
|
|
recall, precision = result["recall"], result["precision"]
|
||
|
|
low, high = recall["wilson95"]
|
||
|
|
exploit = result["exploit_verified"]
|
||
|
|
pending = f" ({len(exploit['not_yet_proved'])} not yet proved)" if exploit["not_yet_proved"] else ""
|
||
|
|
return (
|
||
|
|
f"On unseen app repos: recall {number(recall['rate'], 100)}% "
|
||
|
|
f"(Wilson {number(low, 100)}-{number(high, 100)}), "
|
||
|
|
f"false alarms {number(result['false_alarms_per_100_pushes']['rate'])} per 100 pushes, "
|
||
|
|
f"precision {number(precision['rate'], 100)}%; exploit-verified "
|
||
|
|
f"{exploit['count']} of {exploit['flagged_positives']}{pending}."
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def main(argv=None):
|
||
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
||
|
|
parser.add_argument("records", type=Path, help="joined replay records or replay result directory")
|
||
|
|
parser.add_argument("--scored", type=Path, help="score.py output for these records")
|
||
|
|
parser.add_argument("--proofs", type=Path, help="JSON array of case_id plus search verification records")
|
||
|
|
parser.add_argument("--out", type=Path, required=True)
|
||
|
|
args = parser.parse_args(argv)
|
||
|
|
result = summarize(
|
||
|
|
score.load_records(args.records),
|
||
|
|
scored=json.loads(args.scored.read_text()) if args.scored else None,
|
||
|
|
proofs=json.loads(args.proofs.read_text()) if args.proofs else [],
|
||
|
|
)
|
||
|
|
args.out.write_text(json.dumps(result, sort_keys=True, indent=2) + "\n")
|
||
|
|
print(result["headline"])
|
||
|
|
return 0
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
raise SystemExit(main())
|