spec(pre-push-safety-net): freeze the independent population v2

The analyzer was not run. freeze.py read each case's source at both pins:
- both pins are commits in the repository and in the right order;
- the declared sink site's file is present at the vulnerable pin, and its declared
  functions appear in it;
- the fix touches a declared site file (Budibase's sites reordered so the changed
  file, queries.ts, comes first).

Benign controls:
- 12 are reviewed automatic proposals.
- 3 automatic proposals were rejected as not benign: qwed's merged a ReDoS fix,
  ReactPress's corrected a SQL column whitelist, and Sulu's was a branch merge that
  can carry the fix.
- Those three, plus cct-studio and Directorist (no automatic proposal), were chosen
  by hand. Directorist's comes from before the vulnerable pin, because only 3
  commits follow its fix.
freeze.py now accepts a recorded benign control instead of reporting that none was
proposed.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
This commit is contained in:
Marius Ciepluch
2026-09-26 18:02:03 +02:00
co-authored by Claude Opus 5.5
parent c64e37edbd
commit 21634ccf58
3 changed files with 505 additions and 4 deletions
+9 -2
View File
@@ -112,7 +112,14 @@ def verify(case: dict[str, object], cache: Path) -> dict[str, object]:
record["vulnerable_is_ancestor_of_fixed"] = ordered
if not ordered:
problems.append("the vulnerable pin is not an ancestor of the fixed pin")
sink_file, functions = sink_parts(str(case["sink"]))
sites = [str(site) for site in case.get("sites", [])] # type: ignore[union-attr]
if sites:
# Declared `path::function` sites (population v2) are what the verification reads; the first names the
# sink file, and every function declared in that file must appear in it.
sink_file = sites[0].partition("::")[0]
functions = [site.partition("::")[2] for site in sites if site.partition("::")[0] == sink_file and not site.endswith("::<global>")]
else:
sink_file, functions = sink_parts(str(case["sink"]))
record["sink_file"], record["sink_functions"] = sink_file, functions
before, after = blob(repo, vulnerable, sink_file), blob(repo, fixed, sink_file)
if before is None:
@@ -142,7 +149,7 @@ def verify(case: dict[str, object], cache: Path) -> dict[str, object]:
languages = [str(case["language"]), *[str(x) for x in case.get("also", [])]] # type: ignore[union-attr]
extensions = tuple(e for lang in languages for e in EXTENSIONS.get(lang, ()))
record["benign_proposal"] = benign(repo, fixed, sink_file, extensions)
if record["benign_proposal"] is None:
if record["benign_proposal"] is None and not case.get("benign"):
problems.append("no benign change found automatically; choose one by hand")
return record