feat(model): one defect is one finding, however many regions reach it

contributor-scan 2.9. Task 2.4 let a region's question follow the call
graph, and the moment it did, many entry points could reach one shared
sink: libpng reported the same site twenty-six times. A contributor shown
the same defect twenty-six times learns to scroll past it.

Findings are deduplicated on (site, family), keeping the strongest rung
and counting the regions that reached it -- "reached from 26 regions" is
the useful half of that observation and one row is the readable half. Two
families at one site stay two findings, because `echo $_GET[...]` is an
injection question and an output-encoding question with different answers.

The duplication that motivated this was removed by another route: the 26
rows were all the `fgets`-matched-as-`gets` false positive, and the sink
matcher fix took them out. So this changes no current number and is a
structural guarantee rather than a measured reduction -- the mechanism that
produced them is untouched, and only the finding that made it visible is
gone.

+25 lines for a guarantee, in a session that has been asked to watch its
growth. The audit is unchanged at 97 modules, 90 load-bearing and 7
standalone, with no orphans.

Gates byte-identical.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01T7XD7x5KonMdmGRkp63EQG
This commit is contained in:
Marius Ciepluch
2026-09-09 15:56:01 +02:00
co-authored by Claude Opus 5
parent 60d2a1a92c
commit 47b00d739b
5 changed files with 79 additions and 4 deletions
+37
View File
@@ -327,3 +327,40 @@ def test_the_scan_reports_query_time_per_kind() -> None:
assert set(result.query_seconds_by_kind) == {"taint", "config"}
assert sum(result.query_seconds_by_kind.values()) <= result.query_seconds + 0.05
def test_one_defect_is_one_finding_however_many_regions_reach_it(tmp_path) -> None:
"""contributor-scan 2.9. Task 2.4 let a region's question follow the call graph, and the moment it did,
many entry points could reach one shared sink: libpng reported the same site twenty-six times. A
contributor shown the same defect twenty-six times learns to scroll past it."""
from openultrasast.model.ladder import Rung
from openultrasast.model.pipeline import ModelFinding
from openultrasast.model.scan import _deduplicated
same = [
(ModelFinding(site="wpng.c:335:main", family="memory", rung=Rung.SUSPICION, witness="w"), 0.2),
(ModelFinding(site="wpng.c:335:main", family="memory", rung=Rung.ENTAILED, witness="w"), 1.0),
(ModelFinding(site="wpng.c:335:main", family="memory", rung=Rung.SUSPICION, witness="w"), 0.5),
]
kept = _deduplicated(same)
assert len(kept) == 1
finding, rank = kept[0]
assert finding.rung is Rung.ENTAILED, "the strongest rung survives"
assert finding.reached_from == 3, "and the count is what the duplication was worth"
assert rank == 1.0
def test_two_families_at_one_site_stay_two_findings() -> None:
"""`echo $_GET[...]` is an injection question and an output-encoding question, with different answers."""
from openultrasast.model.ladder import Rung
from openultrasast.model.pipeline import ModelFinding
from openultrasast.model.scan import _deduplicated
both = [
(ModelFinding(site="app.php:9:show", family="injection", rung=Rung.ENTAILED), 1.0),
(ModelFinding(site="app.php:9:show", family="output_encoding", rung=Rung.ENTAILED), 1.0),
]
assert len(_deduplicated(both)) == 2