2026-10-13 12:33:58 +02:00
|
|
|
"""Checks that search scoring follows the benchmark's location rule."""
|
|
|
|
|
|
|
|
|
|
from types import SimpleNamespace
|
|
|
|
|
|
|
|
|
|
import pytest
|
|
|
|
|
|
|
|
|
|
from benchmarks.unseen.score import catches
|
|
|
|
|
from openultrasast.hunt.metric import search_metric, search_outcome
|
|
|
|
|
|
|
|
|
|
|
2026-10-14 02:49:44 +02:00
|
|
|
@pytest.mark.parametrize(
|
|
|
|
|
"change",
|
|
|
|
|
[
|
|
|
|
|
{"family": "injection", "path": "app.py", "function": "handler", "function_lines": [100, 140]},
|
|
|
|
|
{"family": "path", "path": "other.py", "function": "read", "function_lines": [100, 140]},
|
|
|
|
|
{"family": "config_secrets", "path": "app.py", "function": "handler", "function_lines": [100, 140]},
|
|
|
|
|
],
|
|
|
|
|
)
|
2026-10-13 12:33:58 +02:00
|
|
|
def test_matches_score_catches(change):
|
|
|
|
|
change = {**change, "kind": "vulnerability", "files": [{"path": "app.py", "head_lines": [[20, 22]]}]}
|
|
|
|
|
for path in ("app.py", "other.py"):
|
|
|
|
|
for line in (9, 10, 22, 32, 33, 110, 500):
|
|
|
|
|
for function in ("handler", "other"):
|
|
|
|
|
for family in (change["family"], "unknown"):
|
|
|
|
|
finding = {"path": path, "line": line, "function": function, "family": family}
|
2026-10-14 02:49:44 +02:00
|
|
|
record = {**change, "push": {"admission": {"defects": [{**finding, "locations": [[path, line]]}]}}}
|
2026-10-13 12:33:58 +02:00
|
|
|
assert search_outcome(change, [finding]).hit == bool(catches(record))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_counts_scope_feedback_and_tolerance():
|
2026-10-14 02:49:44 +02:00
|
|
|
change = {
|
|
|
|
|
"kind": "vulnerability",
|
|
|
|
|
"path": "app.py",
|
|
|
|
|
"function": "handle",
|
|
|
|
|
"family": "path",
|
|
|
|
|
"function_lines": [20, 22],
|
|
|
|
|
"files": [{"path": "app.py", "head_lines": [[20, 22]]}],
|
|
|
|
|
}
|
|
|
|
|
findings = [{"path": "app.py", "line": 10, "family": "path"}, {"path": "other.py", "line": 30, "family": "path"}]
|
2026-10-13 12:33:58 +02:00
|
|
|
outcome = search_outcome(change, findings)
|
|
|
|
|
assert (outcome.hit, outcome.suspicions, outcome.on_diff) == (True, 2, 1)
|
|
|
|
|
assert "other.py:30" in outcome.feedback
|
|
|
|
|
missed = search_outcome(change, findings, tolerance=0)
|
|
|
|
|
assert not missed.hit
|
|
|
|
|
assert "missed app.py:20 handle family path" in missed.feedback
|
|
|
|
|
assert search_metric({"change": change}, {"findings": findings}) == 1.0
|
|
|
|
|
assert search_metric(SimpleNamespace(change=change), SimpleNamespace(findings=[])) == 0.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.parametrize("count,expected", [(0, 1.0), (1, 0.8), (5, 0.0), (7, 0.0)])
|
|
|
|
|
def test_ordinary_metric(count, expected):
|
|
|
|
|
assert search_metric({"kind": "ordinary", "files": []}, {"findings": [{}] * count}) == expected
|