mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-17 03:59:28 +00:00
feat(learning-harness): family scoring on the pairs hunter path
Task 3.2. The model-driven hunter is now scored by family and function. A detection is a finding of the labeled family inside the labeled function, a finding of another family or outside it is counted and never called a leak, and the words in a finding are never read. A pair that cannot be scored is reported with its reason and the detector is never run on it. The scoreboard gains a per-family block with its own denominator, taxonomy version and the note that hierarchical partial credit could not apply. The text-token matcher this path used is deleted rather than left dormant. It was the reason the hunter's published number said zero of seventeen while the model was finding the right bugs in the right functions, and leaving it in the file would invite its return. The test that pinned the old rule now pins the new one: the same finding with the same words counts when it carries a family tag and does not when it carries none. The overlay and inventory paths and all three gate outputs are unchanged. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01T7XD7x5KonMdmGRkp63EQG
This commit is contained in:
co-authored by
Claude Opus 5
parent
c14ac8344f
commit
fcda1ee46b
+31
-27
@@ -373,7 +373,8 @@ def test_known_limit_pairs_are_evaluated_but_excluded_from_achievable(tmp_path:
|
||||
assert result.degradations and result.degradations[0]["reason"] == "hunter_model_unavailable"
|
||||
|
||||
|
||||
def test_hunter_scorer_uses_same_rules_with_a_scripted_client(tmp_path: Path) -> None:
|
||||
def test_hunter_scorer_credits_a_family_tag_and_never_the_words(tmp_path: Path) -> None:
|
||||
"""learning-harness Req 3.1/3.6 replaced this path's rule: the family tag decides, the rationale never does."""
|
||||
from openultrasast.findings import StaticFinding
|
||||
|
||||
src = "from flask import request\n\ndef f():\n return eval(request.args.get('x'))\n"
|
||||
@@ -387,34 +388,37 @@ def test_hunter_scorer_uses_same_rules_with_a_scripted_client(tmp_path: Path) ->
|
||||
mechanism="source_reaches_sink",
|
||||
)
|
||||
|
||||
def scripted(root: Path) -> list[StaticFinding]:
|
||||
text = (root / "app.py").read_text()
|
||||
if "eval(" not in text:
|
||||
return []
|
||||
return [
|
||||
StaticFinding(
|
||||
finding_id="tool-hunter:app.py:4",
|
||||
path="app.py",
|
||||
title="eval of request input",
|
||||
severity="high",
|
||||
confidence="medium",
|
||||
evidence_level="suspicion",
|
||||
rationale="CWE-95 eval executes attacker input",
|
||||
line=4,
|
||||
function_name=None,
|
||||
reachability_status="unknown",
|
||||
reachability_evidence=[],
|
||||
reachability_conditions=[],
|
||||
tags=[],
|
||||
ranking_priority=1.0,
|
||||
)
|
||||
]
|
||||
def scripted(tags: list[str]): # type: ignore[no-untyped-def]
|
||||
def scan(root: Path) -> list[StaticFinding]:
|
||||
if "eval(" not in (root / "app.py").read_text():
|
||||
return []
|
||||
return [
|
||||
StaticFinding(
|
||||
finding_id="tool-hunter:app.py:4",
|
||||
path="app.py",
|
||||
title="eval of request input",
|
||||
severity="high",
|
||||
confidence="medium",
|
||||
evidence_level="suspicion",
|
||||
rationale="CWE-95 eval executes attacker input",
|
||||
line=4,
|
||||
function_name=None,
|
||||
reachability_status="unknown",
|
||||
reachability_evidence=[],
|
||||
reachability_conditions=[],
|
||||
tags=tags,
|
||||
ranking_priority=1.0,
|
||||
)
|
||||
]
|
||||
|
||||
return scan
|
||||
|
||||
case = _case(tmp_path, "hunt", src, "def f():\n return 1\n", row)
|
||||
result = evaluate_catalog([case], hunter=scripted)
|
||||
hunter = result.scorers["sast"]["hunter"]
|
||||
assert hunter.pair_correct == 1 and hunter.detected_vuln == 1 and hunter.silent_fix == 1
|
||||
assert not result.degradations
|
||||
tagged = evaluate_catalog([case], hunter=scripted(["family:injection"])).scorers["sast"]["hunter"]
|
||||
assert tagged.pair_correct == 1 and tagged.detected_vuln == 1 and tagged.silent_fix == 1
|
||||
# the same finding, the same words, no family tag: not a detection, because the rationale is not evidence
|
||||
untagged = evaluate_catalog([case], hunter=scripted([])).scorers["sast"]["hunter"]
|
||||
assert untagged.detected_vuln == 0 and untagged.silent_fix == 1
|
||||
|
||||
|
||||
def test_juliet_strcpy_pair_bodies_differ_and_java_hash_is_known_limit() -> None:
|
||||
|
||||
Reference in New Issue
Block a user