Files
Open_Ultra_SAST/tests/test_pair_hygiene.py
T
Marius CiepluchandClaude Opus 5 1af3db37f6 feat(corpus): vendor 40 OWASP xss pairs as the owasp slice
output_encoding had exactly ONE pair in the whole corpus. It now has 41.

238 pairs were harvested from OWASP BenchmarkJava (2740 generated servlets,
GPL-2.0, with a ground-truth CSV) by matching each really-vulnerable test to
the safe test of the same category it most resembles -- adjacent tests come
from one template, so they differ in the sink or the sanitiser rather than
being unrelated programs. Median similarity 0.91, against the 64% of the older
`sast` OWASP entries that the group-4 stratification flagged as different
programs.

Only 40 are vendored. One representative of each of the seven categories was
run through the model BEFORE vendoring, and only xss distinguished: its fix
adds ESAPI encoding, which the Java sanitizer facts recognise, so the
vulnerable side entails and the fixed side drops to corroborated. cmdi, sqli,
ldapi and xpathi put the sink in a helper class outside the servlet and
resolved no flow at all; pathtraver found the flow but its "safe" twin carries
no distinguishing call, so text similarity had matched a twin that is not
semantically the fix -- a subtler form of the very defect the harvest was
designed to avoid. Those 198 pairs are deliberately left out: adding pairs the
model cannot read would inflate the corpus while making every number less
interpretable.

Evidence for the vendored slice is 3 of 3 distinguished, which is a small
sample and is stated as such in the catalog header rather than implied to be
more.

The slice is marked SYNTHETIC in its header: it measures breadth across a
weakness class and must never stand in for real-world performance. The corpus
already shows a +16% overfitting gap, and a large generated slice blended into
a headline would widen it invisibly.

Two things recorded rather than hidden: BenchmarkJava's LICENSE is GPL-2.0
while the older sast entries declare GPL-3.0 for OWASP, which looks like an
error and is not copied; and OWASP-Benchmark/BenchmarkJava already straddled
the train/holdout split before these pairs (owasp-java-cmdi train,
owasp-java-sqli holdout), so the new all-holdout pairs enlarge an existing
straddle rather than create one.

The two vendored-count guards did their job -- a corpus change could not land
silently -- and are updated as the acknowledgement. Hygiene artifact
regenerated: 277 -> 317 vendored pairs.

720 passing, mypy/ruff clean, gates byte-identical.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01T7XD7x5KonMdmGRkp63EQG
2026-09-08 17:28:11 +02:00

244 lines
12 KiB
Python

"""learning-harness task 1.3: additive corpus fields and hygiene at load (offline)."""
from __future__ import annotations
import json
import re
from pathlib import Path
from openultrasast.pairs import DEFAULT_CATALOG, PairCase, load_pair_catalog, select_vendored
IDENTICAL_TWINS = {
"dsvw-dsvw-do-get",
"python-insecure-app-main-try-hack-me",
"pythonssti-main-read-root",
"vampi-books-get-by-title",
"vulnpy-deserialization-do-pickle-load",
}
# `owasp` added 2026-09-08: 40 generated BenchmarkJava xss pairs, the only harvested category the model was
# shown to distinguish before vendoring. This guard exists so a corpus change is never silent — updating it
# is the acknowledgement.
VENDORED_PER_SLICE = {"local": 3, "github": 6, "sast": 11, "vibe-py": 35, "vfc-js": 17, "vfc": 176,
"agent-vfc": 29, "owasp": 40}
def test_identical_twins_are_unscorable_and_no_row_is_dropped() -> None:
cases = load_pair_catalog(DEFAULT_CATALOG)
per_slice: dict[str, int] = {}
for case in select_vendored(cases):
per_slice[case.slice] = per_slice.get(case.slice, 0) + 1
assert per_slice == VENDORED_PER_SLICE # nothing dropped by the new accounting
twins = {case.name for case in cases if case.unscorable == "identical_twin"}
assert twins == IDENTICAL_TWINS
# The declaration and the bytes must still agree. A declared limit that stopped being true after a re-harvest
# would otherwise be invisible: the mirror image of the risk declaring it was meant to close.
from openultrasast.pairs import _body_digest
declared = [case for case in cases if case.known_limit == "identical_twin"]
assert {case.name for case in declared} == IDENTICAL_TWINS
assert all(_body_digest(case.vuln_file) == _body_digest(case.fixed_file) for case in declared)
assert all(case.unscorable is None for case in cases if case.name not in IDENTICAL_TWINS and not case.known_limit)
def test_a_declared_known_limit_wins_over_a_computed_reason(tmp_path: Path) -> None:
from openultrasast.pairs import _load_catalog_file
(tmp_path / "v.py").write_text("# provenance: x\n\ndef f():\n return 1\n")
(tmp_path / "f.py").write_text("# provenance: y\n\ndef f():\n return 1\n")
head = (
'[[pair]]\nname = "t"\nslice = "sast"\nvuln = "v.py"\nfixed = "f.py"\nrelpath = "app.py"\n{extra}\n'
'[[pair.expected]]\ncwe = "CWE-95"\nclass = "x"\npath = "app.py"\nsink = "eval"\nmechanism = "other"\n'
)
(tmp_path / "plain.toml").write_text(head.format(extra=""))
(case,) = _load_catalog_file(tmp_path / "plain.toml")
assert case.unscorable == "identical_twin" # the provenance headers differ, the bodies do not
(tmp_path / "limited.toml").write_text(head.format(extra='known_limit = "java hash"'))
(limited,) = _load_catalog_file(tmp_path / "limited.toml")
assert limited.unscorable == "known_limit:java hash"
def test_expected_rows_carry_an_opaque_family_label(tmp_path: Path) -> None:
from openultrasast.pairs import _load_catalog_file
(tmp_path / "v.py").write_text("x = 1\n")
(tmp_path / "f.py").write_text("x = 2\n")
row = (
'[[pair]]\nname = "t"\nslice = "sast"\nvuln = "v.py"\nfixed = "f.py"\nrelpath = "app.py"\n\n'
'[[pair.expected]]\ncwe = "CWE-95"\nclass = "x"\npath = "app.py"\nsink = "eval"\nmechanism = "other"\nfamily = "{family}"\n'
)
(tmp_path / "c.toml").write_text(row.format(family="injection"))
(case,) = _load_catalog_file(tmp_path / "c.toml")
assert case.expected[0].family == "injection"
# the corpus stores the label; the classifier owns the taxonomy, so a fabricated id is not rejected here
(tmp_path / "d.toml").write_text(row.format(family="not_a_family"))
(other,) = _load_catalog_file(tmp_path / "d.toml")
assert other.expected[0].family == "not_a_family"
def test_duplicate_bodies_across_pairs_are_reported_before_any_split() -> None:
from openultrasast.pairs import duplicate_groups
groups = duplicate_groups(select_vendored(load_pair_catalog(DEFAULT_CATALOG)))
assert all(len(names) > 1 for names in groups)
assert all(len(set(names)) == len(names) for names in groups)
report = Path("benchmarks/measurements/2026-09-06-corpus-hygiene.json")
assert report.is_file(), "the hygiene report is committed beside the numbers it explains"
payload = json.loads(report.read_text())
assert payload["duplicate_groups"] == [list(names) for names in groups]
assert set(payload["identical_twins"]) == IDENTICAL_TWINS
assert groups == tuple(sorted(groups)), "the committed report must not reshuffle when one excerpt is re-harvested"
def test_duplicate_groups_that_cross_the_split_are_named_as_the_leak_they_are() -> None:
"""Two byte-identical excerpts on opposite sides of the split teach the holdout row exactly.
`split_by_repository` cannot see this: the duplicates are different repositories (a fork of a project, or one CVE
harvested under two names), so the repository-level split reports no straddle and the leak stays invisible."""
from openultrasast.pairs import duplicate_groups
cases = select_vendored(load_pair_catalog(DEFAULT_CATALOG))
splits = {case.name: case.split for case in cases}
crossing = [list(names) for names in duplicate_groups(cases) if len({splits[name] for name in names}) > 1]
payload = json.loads(Path("benchmarks/measurements/2026-09-06-corpus-hygiene.json").read_text())
assert payload["cross_split_duplicates"] == crossing
assert len(crossing) == 9, "nine of the ten duplicate groups cross the split; the number is the finding"
assert ["openniw-jobs-0fc947", "openniw-jobs-b74de3"] in crossing # two forks of one project, train and holdout
def test_split_by_repository_keeps_declared_splits_and_reports_straddles() -> None:
from openultrasast.pairs import split_by_repository
cases = select_vendored(load_pair_catalog(DEFAULT_CATALOG))
assigned, report = split_by_repository(cases)
declared = {case.name: case.split for case in cases if case.split_declared}
assert {case.name: case.split for case in assigned if case.name in declared} == declared # never re-splits a declared row
assert {name for name, _ in report.assigned} == {case.name for case in cases if not case.split_declared}
assert report.straddling, "repositories that sit in both splits are a corpus defect worth naming"
payload = json.loads(Path("benchmarks/measurements/2026-09-06-corpus-hygiene.json").read_text())
assert payload["straddling"] == [[repo, list(names)] for repo, names in report.straddling]
assert payload["undeclared_split"] == [list(item) for item in report.assigned]
def test_split_by_repository_assigns_undeclared_rows_by_repository_and_date(tmp_path: Path) -> None:
from openultrasast.pairs import split_by_repository
def case(name: str, repo: str, date: str = "") -> PairCase:
(tmp_path / f"{name}.py").write_text(f"# {name}\n")
return PairCase(
name=name,
slice="sast",
language="python",
origin="test",
vuln_file=tmp_path / f"{name}.py",
fixed_file=tmp_path / f"{name}.py",
relpath="app.py",
expected=(),
min_recall=1.0,
fix_policy="silent",
repo=repo,
split="",
split_declared=False,
fix_date=date,
)
cases = [case("a", "one", "2024-01-01"), case("b", "one", "2026-01-01"), case("c", "two", "2025-01-01"), case("d", "two", "2025-06-01")]
assigned, report = split_by_repository(cases, holdout_fraction=0.5)
by_name = {item.name: item.split for item in assigned}
assert by_name["a"] == by_name["b"] and by_name["c"] == by_name["d"] # a repository never straddles
assert {by_name["a"], by_name["c"]} == {"train", "holdout"}
assert by_name["a"] == "train" # oldest repository trains, newest is held out
assert dict(report.assigned) == by_name and report.straddling == () and report.undated == 0
def test_pair_signals_can_be_filtered_to_one_split() -> None:
from openultrasast.pairs import PairOutcome, build_pair_signals
def outcome(name: str, split: str) -> PairOutcome:
return PairOutcome(
name=name,
slice="sast",
language="python",
origin="t",
detected_vuln=False,
silent_fix=True,
pair_correct=False,
vuln_matched=0,
vuln_expected=1,
vuln_findings=0,
fix_findings=0,
fix_leaks=0,
recall=0.0,
misses=("r:app.py:CWE-95",),
leaks=(),
split=split,
)
outcomes = [outcome("train-one", "train"), outcome("holdout-one", "holdout")]
assert {signal["pair"] for signal in build_pair_signals(outcomes)} == {"train-one", "holdout-one"}
assert {signal["pair"] for signal in build_pair_signals(outcomes, split="train")} == {"train-one"}
HEADER_C = "/* Provenance: acme/lib bad ({side}).\n * repo: acme/lib\n * commit: abc\n * license: MIT\n */\n"
HEADER_PY = "# Provenance: acme/lib f ({side}).\n# repo: acme/lib\n# license: MIT\n"
def _twin(tmp_path: Path, suffix: str, vuln: str, fixed: str) -> object:
from openultrasast.pairs import _load_catalog_file
(tmp_path / f"v{suffix}").write_text(vuln)
(tmp_path / f"f{suffix}").write_text(fixed)
(tmp_path / "c.toml").write_text(
f'[[pair]]\nname = "t"\nslice = "sast"\nvuln = "v{suffix}"\nfixed = "f{suffix}"\nrelpath = "app{suffix}"\n\n'
'[[pair.expected]]\ncwe = "CWE-121"\nclass = "x"\npath = "app.c"\nsink = "strcpy"\nmechanism = "other"\n'
)
(case,) = _load_catalog_file(tmp_path / "c.toml")
return case
def test_a_preprocessor_directive_is_code_not_a_provenance_header(tmp_path: Path) -> None:
"""The whole point of the CWE-121 pairs is the buffer size, and it lives on a `#define`."""
body = "#include <string.h>\n#define BUFSIZE {size}\n\nvoid bad(void) {{\n char buf[BUFSIZE];\n strcpy(buf, src);\n}}\n"
case = _twin(
tmp_path,
".c",
HEADER_C.format(side="vuln") + body.format(size="10"),
HEADER_C.format(side="fixed") + body.format(size="4096"),
)
assert case.unscorable is None # type: ignore[attr-defined]
def test_a_leading_code_comment_is_not_a_provenance_header(tmp_path: Path) -> None:
same = "def f(value):\n return value\n"
case = _twin(
tmp_path,
".py",
HEADER_PY.format(side="vuln") + "# this function is fine\n" + same,
HEADER_PY.format(side="fixed") + "# this function is not fine\n" + same,
)
assert case.unscorable is None # a comment that is not provenance metadata is part of the file
def test_the_provenance_header_itself_is_still_stripped(tmp_path: Path) -> None:
same = "def f(value):\n return value\n"
case = _twin(tmp_path, ".py", HEADER_PY.format(side="vuln") + same, HEADER_PY.format(side="fixed") + same)
assert case.unscorable == "identical_twin" # only the header differs, so the code is the same code
block = _twin(
tmp_path, ".c", HEADER_C.format(side="vuln") + "void bad(void) {}\n", HEADER_C.format(side="fixed") + "void bad(void) {}\n"
)
assert block.unscorable == "identical_twin" # the C block comment form too
def test_no_vendored_excerpt_loses_a_preprocessor_directive_to_the_header_strip() -> None:
from openultrasast.pairs import _header_lines
eaten: list[str] = []
for case in select_vendored(load_pair_catalog(DEFAULT_CATALOG)):
for path in (case.vuln_file, case.fixed_file):
if not path.is_file():
continue
lines = path.read_text(errors="ignore").splitlines()
stripped = lines[: _header_lines(lines)]
if any(re.match(r"\s*#\s*(include|define|if|ifdef|ifndef|elif|else|endif|pragma|undef|import)\b", line) for line in stripped):
eaten.append(f"{case.name}:{path.name}")
assert eaten == []