mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-16 11:49:28 +00:00
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:
co-authored by
Claude Opus 5
parent
60d2a1a92c
commit
47b00d739b
@@ -969,6 +969,17 @@ def _run_model_layer(
|
||||
return findings, _model_payload(result, unjudged_paths=unjudged, coverage=coverage)
|
||||
|
||||
|
||||
def _model_rationale(item: object, witness: str, family: str) -> str:
|
||||
"""The witness, plus how many entry points reach this site when more than one does.
|
||||
|
||||
A site reached from twenty-six entry points is one defect with a wide blast radius, which is worth
|
||||
saying once. It used to be said twenty-six times.
|
||||
"""
|
||||
base = witness or f"the model layer reported a {family} candidate"
|
||||
reached = int(getattr(item, "reached_from", 1) or 1)
|
||||
return base if reached <= 1 else f"{base} (reached from {reached} regions)"
|
||||
|
||||
|
||||
def _finding_from_model(item: object) -> StaticFinding:
|
||||
"""A model finding as a StaticFinding, carrying its rung and witness (Req 5.1)."""
|
||||
site = str(getattr(item, "site", ""))
|
||||
@@ -984,7 +995,7 @@ def _finding_from_model(item: object) -> StaticFinding:
|
||||
severity="medium",
|
||||
confidence="medium",
|
||||
evidence_level="suspicion",
|
||||
rationale=witness or f"the model layer reported a {family} candidate",
|
||||
rationale=_model_rationale(item, witness, family),
|
||||
line=int(line) if line.isdigit() else None,
|
||||
function_name=function or None,
|
||||
reachability_status="unknown",
|
||||
|
||||
@@ -64,6 +64,9 @@ class ModelFinding:
|
||||
rung: Rung
|
||||
witness: str = ""
|
||||
contradiction: str = ""
|
||||
# How many regions reached this site. One defect is one finding however many entry points lead to it;
|
||||
# the count is the useful part of the duplication, and the rows are not.
|
||||
reached_from: int = 1
|
||||
|
||||
|
||||
def _arbitrate_all(
|
||||
|
||||
@@ -21,7 +21,7 @@ from __future__ import annotations
|
||||
import logging
|
||||
import time
|
||||
from collections.abc import Mapping, Sequence
|
||||
from dataclasses import dataclass, field
|
||||
from dataclasses import dataclass, field, replace
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
@@ -206,7 +206,7 @@ def scan_repository(
|
||||
|
||||
scanned = len(judged)
|
||||
|
||||
findings = _ordered(collected)
|
||||
findings = _ordered(_deduplicated(collected))
|
||||
return ModelScanResult(
|
||||
findings=findings,
|
||||
by_rung=_tally(findings),
|
||||
@@ -289,6 +289,30 @@ def _spec_for(family: str, language: str) -> ArbiterSpec | None:
|
||||
return taint_specs(language=language).get(family)
|
||||
|
||||
|
||||
def _deduplicated(collected: Sequence[tuple[ModelFinding, float]]) -> list[tuple[ModelFinding, float]]:
|
||||
"""One finding per (site, family), keeping the strongest rung and counting the regions that reached it.
|
||||
|
||||
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, and "reached from 26 entry points" is the useful half
|
||||
of that observation.
|
||||
|
||||
Two families at one site stay two findings: `echo $_GET[...]` is an injection question and an output
|
||||
encoding question, and they have different answers.
|
||||
"""
|
||||
best: dict[tuple[str, str], tuple[ModelFinding, float]] = {}
|
||||
for finding, rank in collected:
|
||||
key = (finding.site, finding.family)
|
||||
current = best.get(key)
|
||||
if current is None:
|
||||
best[key] = (finding, rank)
|
||||
continue
|
||||
kept, kept_rank = current
|
||||
stronger = finding if _RUNG_ORDER[finding.rung] < _RUNG_ORDER[kept.rung] else kept
|
||||
best[key] = (replace(stronger, reached_from=kept.reached_from + finding.reached_from), max(rank, kept_rank))
|
||||
return list(best.values())
|
||||
|
||||
|
||||
def _ordered(collected: Sequence[tuple[ModelFinding, float]]) -> tuple[ModelFinding, ...]:
|
||||
"""Rung first, then the region's rank, then the site — a total order, so two runs agree."""
|
||||
return tuple(finding for finding, _ in sorted(collected, key=lambda pair: (_RUNG_ORDER.get(pair[0].rung, 9), -pair[1], pair[0].site)))
|
||||
|
||||
Reference in New Issue
Block a user