feat(real-world-vfc-slice): add labeled OpenSSL/Firefox/Chromium/curl pair corpus

Harvest 176 isolated vuln-vs-fix functions (109 CVEs, 352 training rows)
into benchmarks/pairs/vfc with provenance and a JSONL training manifest.
Score the slice as an honesty dashboard (ousast pairs --slice vfc);
keep the pair gate on the local fixtures only.
This commit is contained in:
Marius Ciepluch
2026-09-04 16:45:38 +02:00
parent e9f185da59
commit 0116f9bea6
372 changed files with 49209 additions and 49 deletions
+1 -1
View File
@@ -125,7 +125,7 @@ def main(argv: list[str] | None = None) -> int:
help="scan isolated vuln-vs-fixed pairs (TP on vuln, silent on fix) and emit improve-loop signals",
)
pairs.add_argument("--catalog", type=Path, default=DEFAULT_CATALOG)
pairs.add_argument("--slice", choices=("all", "local", "github", "sast"), default="all")
pairs.add_argument("--slice", choices=("all", "local", "github", "sast", "vfc"), default="all")
pairs.add_argument("--json", action="store_true", help="print the pair scoreboard as JSON")
subparsers.add_parser("mcp", help="run the narrow MCP server over stdio for OpenCode integration")
+10 -4
View File
@@ -1,9 +1,9 @@
"""Pair-efficiency gate: local fixtures must fire on vuln and stay silent on fix.
The stage-1 smoke gate stays on cheat-sheet trees. This module measures
differential detection and only *fails CI* on the local slice. GitHub/SVEN
pairs are printed as the honesty dashboard (real VFC misses become improve
signals, they do not block merge).
differential detection and only *fails CI* on the local slice. GitHub/SVEN,
sast, and vfc pairs are honesty dashboards (`ousast pairs --slice ...`);
they are not evaluated here so a large vfc catalog cannot slow the merge gate.
python -m openultrasast.pair_gate
"""
@@ -36,7 +36,7 @@ def pair_gate(cases: Sequence[PairCase] | None = None) -> PairGateVerdict:
local = select_slice(catalog, "local")
if not local:
return PairGateVerdict(passed=False, reasons=("no local pairs in catalog",), result=evaluate_catalog(()))
result = evaluate_catalog(catalog)
result = evaluate_catalog(local)
local_metrics = result.per_slice.get("local")
reasons: list[str] = []
if local_metrics is None:
@@ -62,6 +62,12 @@ def print_pair_metrics(label: str, result: PairEvalResult) -> None:
f"vuln {metrics.vuln_recall:>6.2%} silent {metrics.specificity:>6.2%} "
f"Youden {metrics.youden:>+7.2%} labeled {metrics.labeled_recall:>6.2%}"
)
for slice_name, inner in result.scorers.items():
for scorer, metrics in inner.items():
print(
f" {slice_name:<8} scorer={scorer:<10} pair_pass {metrics.pair_pass_rate:>6.2%} "
f"({metrics.pair_correct}/{metrics.pairs}) Youden {metrics.youden:>+7.2%}"
)
for outcome in result.outcomes:
mark = "PASS" if outcome.pair_correct else "MISS" if not outcome.detected_vuln else "LEAK"
print(
+70 -40
View File
@@ -11,7 +11,7 @@ import shutil
import tempfile
import tomllib
from collections.abc import Sequence
from dataclasses import asdict, dataclass
from dataclasses import asdict, dataclass, field
from pathlib import Path
from .benchmark import (
@@ -30,7 +30,9 @@ from .semantic import OverlayRecord, adjudicate
DEFAULT_CATALOG = Path("benchmarks/pairs/catalog.toml")
DEFAULT_SAST_CATALOG = Path("benchmarks/pairs/sast/catalog.toml")
DEFAULT_VFC_CATALOG = Path("benchmarks/pairs/vfc/catalog.toml")
DEFAULT_DATASETS = Path("benchmarks/pairs/datasets.toml")
OVERLAY_SLICES = frozenset({"sast", "vfc"})
@dataclass(frozen=True)
@@ -95,6 +97,7 @@ class PairEvalResult:
overall: PairCorpusMetrics
per_slice: dict[str, PairCorpusMetrics]
signals: tuple[dict[str, object], ...]
scorers: dict[str, dict[str, PairCorpusMetrics]] = field(default_factory=dict)
def load_datasets(path: Path = DEFAULT_DATASETS) -> tuple[dict[str, object], ...]:
@@ -104,8 +107,11 @@ def load_datasets(path: Path = DEFAULT_DATASETS) -> tuple[dict[str, object], ...
def load_pair_catalog(path: Path = DEFAULT_CATALOG) -> tuple[PairCase, ...]:
cases = _load_catalog_file(path)
if path.resolve() == DEFAULT_CATALOG.resolve() and DEFAULT_SAST_CATALOG.exists():
cases = cases + _load_catalog_file(DEFAULT_SAST_CATALOG)
if path.resolve() == DEFAULT_CATALOG.resolve():
if DEFAULT_SAST_CATALOG.exists():
cases = cases + _load_catalog_file(DEFAULT_SAST_CATALOG)
if DEFAULT_VFC_CATALOG.exists():
cases = cases + _load_catalog_file(DEFAULT_VFC_CATALOG)
return cases
@@ -142,48 +148,21 @@ def evaluate_pair(case: PairCase) -> PairOutcome:
with tempfile.TemporaryDirectory(prefix="ousast-pair-") as scratch:
vuln_root = _materialize(Path(scratch) / "vuln", case.vuln_file, case.relpath)
fix_root = _materialize(Path(scratch) / "fixed", case.fixed_file, case.relpath)
if case.slice == "sast":
if case.slice in OVERLAY_SLICES:
return _evaluate_overlay_pair(case, vuln_root, fix_root)
vuln_findings = _quick_scan(vuln_root)
fix_findings = _quick_scan(fix_root)
vuln_result = evaluate_benchmark(
run=BenchmarkRun(benchmark_run_id="pair", root=Path("/tmp/pair"), manifest=_manifest(case)),
mode="quick",
findings=vuln_findings,
scan_id=None,
scan_run_dir=None,
)
expected_total = len(case.expected)
matched = vuln_result.metrics.matched_findings_total
recall = matched / expected_total if expected_total else 1.0
leaks = _fix_leaks(case, fix_findings)
detected = recall + 1e-12 >= case.min_recall
silent = not leaks
misses = tuple(f"{miss.rule_id or '-'}:{miss.path}:{miss.cwe}" for miss in vuln_result.misses)
leak_ids = tuple(finding.finding_id for finding in leaks)
return PairOutcome(
name=case.name,
slice=case.slice,
language=case.language,
origin=case.origin,
detected_vuln=detected,
silent_fix=silent,
pair_correct=detected and silent,
vuln_matched=matched,
vuln_expected=expected_total,
vuln_findings=len(vuln_findings),
fix_findings=len(fix_findings),
fix_leaks=len(leaks),
recall=recall,
misses=misses,
leaks=leak_ids,
commit_url=case.commit_url,
cve=case.cve,
)
return _evaluate_inventory_pair(case, vuln_root, fix_root)
def evaluate_catalog(cases: Sequence[PairCase]) -> PairEvalResult:
outcomes = tuple(evaluate_pair(case) for case in cases)
scorers: dict[str, dict[str, PairCorpusMetrics]] = {}
vfc_cases = tuple(case for case in cases if case.slice == "vfc")
if vfc_cases:
overlay_outcomes = tuple(item for item in outcomes if item.slice == "vfc")
scorers["vfc"] = {
"overlay": _metrics(overlay_outcomes),
"inventory": _metrics(tuple(_inventory_only(case) for case in vfc_cases)),
}
return PairEvalResult(
outcomes=outcomes,
overall=_metrics(outcomes),
@@ -192,6 +171,7 @@ def evaluate_catalog(cases: Sequence[PairCase]) -> PairEvalResult:
for slice_name in sorted({item.slice for item in outcomes})
},
signals=tuple(build_pair_signals(outcomes)),
scorers=scorers,
)
@@ -220,6 +200,10 @@ def result_payload(result: PairEvalResult) -> dict[str, object]:
"per_slice": {name: asdict(metrics) for name, metrics in result.per_slice.items()},
"outcomes": [asdict(outcome) for outcome in result.outcomes],
"signals": list(result.signals),
"scorers": {
slice_name: {scorer: asdict(metrics) for scorer, metrics in inner.items()}
for slice_name, inner in result.scorers.items()
},
}
@@ -290,6 +274,52 @@ def _overlay_scan(target: Path) -> tuple[list[StaticFinding], list[OverlayRecord
return findings, adjudicate(root=target, targets=targets, findings=findings)
def _inventory_only(case: PairCase) -> PairOutcome:
with tempfile.TemporaryDirectory(prefix="ousast-pair-inv-") as scratch:
vuln_root = _materialize(Path(scratch) / "vuln", case.vuln_file, case.relpath)
fix_root = _materialize(Path(scratch) / "fixed", case.fixed_file, case.relpath)
return _evaluate_inventory_pair(case, vuln_root, fix_root)
def _evaluate_inventory_pair(case: PairCase, vuln_root: Path, fix_root: Path) -> PairOutcome:
vuln_findings = _quick_scan(vuln_root)
fix_findings = _quick_scan(fix_root)
vuln_result = evaluate_benchmark(
run=BenchmarkRun(benchmark_run_id="pair", root=Path("/tmp/pair"), manifest=_manifest(case)),
mode="quick",
findings=vuln_findings,
scan_id=None,
scan_run_dir=None,
)
expected_total = len(case.expected)
matched = vuln_result.metrics.matched_findings_total
recall = matched / expected_total if expected_total else 1.0
leaks = _fix_leaks(case, fix_findings)
detected = recall + 1e-12 >= case.min_recall
silent = not leaks
misses = tuple(f"{miss.rule_id or '-'}:{miss.path}:{miss.cwe}" for miss in vuln_result.misses)
leak_ids = tuple(finding.finding_id for finding in leaks)
return PairOutcome(
name=case.name,
slice=case.slice,
language=case.language,
origin=case.origin,
detected_vuln=detected,
silent_fix=silent,
pair_correct=detected and silent,
vuln_matched=matched,
vuln_expected=expected_total,
vuln_findings=len(vuln_findings),
fix_findings=len(fix_findings),
fix_leaks=len(leaks),
recall=recall,
misses=misses,
leaks=leak_ids,
commit_url=case.commit_url,
cve=case.cve,
)
def _evaluate_overlay_pair(case: PairCase, vuln_root: Path, fix_root: Path) -> PairOutcome:
vuln_findings, vuln_overlay = _overlay_scan(vuln_root)
fix_findings, fix_overlay = _overlay_scan(fix_root)