Files
Open_Ultra_SAST/tests/test_learning_classifier_report.py
T

112 lines
5.4 KiB
Python
Raw Normal View History

"""learning-harness task 2.2: the classifier reports on itself and queues disagreements (offline)."""
from __future__ import annotations
import json
from pathlib import Path
from openultrasast.benchmark import ExpectedFinding
from openultrasast.learning.families import load_families
from openultrasast.pairs import DEFAULT_CATALOG, PairCase, load_pair_catalog, select_vendored
def _case(tmp_path: Path, name: str, *, cwe: str, mechanism: str = "other", family: str | None = None, tier: str = "reviewed") -> PairCase:
(tmp_path / f"{name}.py").write_text(f"# {name}\nx = 1\n")
return PairCase(
name=name,
slice="vibe-py",
language="python",
origin="test",
vuln_file=tmp_path / f"{name}.py",
fixed_file=tmp_path / f"{name}.py",
relpath="app.py",
expected=(
ExpectedFinding(cwe=cwe, vulnerability_class="x", path="app.py", evidence="", function="f", mechanism=mechanism, family=family),
),
min_recall=1.0,
fix_policy="silent",
review_tier=tier,
reviewer="m" if tier == "reviewed" else "",
)
def test_the_report_covers_the_whole_corpus_and_names_the_unknowns(tmp_path: Path) -> None:
from openultrasast.learning.classify import measure_classifier
taxonomy = load_families()
cases = select_vendored(load_pair_catalog(DEFAULT_CATALOG))
report = measure_classifier(cases, taxonomy, client=None)
assert report.taxonomy_version == taxonomy.version
assert report.classified == len(cases)
assert sum(report.per_tier.values()) == len(cases) and set(report.per_tier) <= {"static", "model", "none"}
assert report.per_family["memory"] >= 150 # the vfc rows land in one family instead of reading `other`
assert report.per_family["access_control"] >= 10 and report.per_family["injection"] >= 40
assert report.unknown == 2 and report.per_tier["static"] == len(cases) - 2 # tier one places all but two rows
payload = report.to_dict()
json.dumps(payload)
assert payload["labeled"] == 0 # no maintainer family labels in the corpus yet, and the report says so
def test_agreement_uses_the_first_answer_and_the_oracle_uses_any_of_them(tmp_path: Path) -> None:
from openultrasast.learning.classify import measure_classifier
taxonomy = load_families()
cases = [
_case(tmp_path, "hit", cwe="CWE-89", family="injection"), # tier one answers injection: agreement and oracle
_case(tmp_path, "miss", cwe="CWE-89", family="access_control"), # answers injection: neither
]
report = measure_classifier(cases, taxonomy, client=None)
assert report.labeled == 2
assert report.agreement == 0.5 and report.oracle_router == 0.5
assert abs(report.random_router - 1 / 9) < 1e-9 # a uniform pick over the nine real families
assert report.confusion["injection"]["injection"] == 1 and report.confusion["access_control"]["injection"] == 1
def test_every_disagreement_is_queued_once_with_both_answers_and_the_tier(tmp_path: Path) -> None:
from openultrasast.learning.classify import measure_classifier, write_review_queue
taxonomy = load_families()
cases = [_case(tmp_path, "miss", cwe="CWE-89", family="access_control"), _case(tmp_path, "hit", cwe="CWE-89", family="injection")]
report = measure_classifier(cases, taxonomy, client=None)
assert [entry.pair for entry in report.queue] == ["miss"]
entry = report.queue[0]
assert entry.label == "access_control" and entry.classified == ("injection",) and entry.tier == "static"
queue_path = tmp_path / "review-queue.jsonl"
write_review_queue(queue_path, report.queue)
write_review_queue(queue_path, report.queue) # a second measurement must not duplicate the row
rows = [json.loads(line) for line in queue_path.read_text().splitlines() if line.strip()]
assert [row["pair"] for row in rows] == ["miss"]
assert rows[0]["label"] == "access_control" and rows[0]["classified"] == ["injection"] and rows[0]["tier"] == "static"
def test_measuring_never_writes_a_label(tmp_path: Path) -> None:
from openultrasast.learning.classify import measure_classifier
taxonomy = load_families()
case = _case(tmp_path, "miss", cwe="CWE-89", family="access_control")
measure_classifier([case], taxonomy, client=None)
assert case.expected[0].family == "access_control" # the classifier proposes; the maintainer decides
def test_only_reviewed_and_seeded_rows_count_towards_agreement(tmp_path: Path) -> None:
from openultrasast.learning.classify import measure_classifier
taxonomy = load_families()
cases = [
_case(tmp_path, "trusted", cwe="CWE-89", family="access_control", tier="reviewed"),
_case(tmp_path, "untrusted", cwe="CWE-89", family="access_control", tier="title"),
]
report = measure_classifier(cases, taxonomy, client=None)
assert report.labeled == 1 and report.classified == 2 # an unreviewed label is not a reference answer
assert [entry.pair for entry in report.queue] == ["trusted"]
def test_the_report_says_when_hierarchical_credit_could_not_apply(tmp_path: Path) -> None:
"""Req 2.6: the shipped taxonomy is flat, so partial credit is inert and every figure must say so."""
from openultrasast.learning.classify import measure_classifier
taxonomy = load_families()
report = measure_classifier([_case(tmp_path, "a", cwe="CWE-89", family="injection")], taxonomy, client=None)
assert report.hierarchical_credit is False
assert report.to_dict()["hierarchical_credit"] is False