mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-18 04:29:27 +00:00
chore(p0): drop never-enabled hunter tools, test-only functions and receiver delivery
Off every active line (PATHFINDER-2026-10-13/03-unified-proposal.md, cuts). Deleted: the four hunter tools no caller enables (read_definition, entry_points, flows, obligations) with their schemas and dispatch, the dead mapping branch of find_refs; 20 functions whose only caller was one test (mapping task records, compute_ranking_metrics, build_quick_hunter_prompt, validate_evidence_transition, should_fuse, harness processors, rank_targets_with_model, model/report slice rendering, taxonomy hierarchical/related, config_value.verdict, taint.verdict, roles.load_roles, features.language_of and build_for_scan, folds.leave_one_*, features.recorded_case, search BatchExecutor and the three __main__ shims, delivery.put_input); the receiver delivery mode of the plane. push_inputs.py moved to benchmarks/push. memory.purge stays: the live-S3 test cleanup uses it. Net: 2,730 lines deleted, 207 added. 473 targeted tests passed; 3,282 tests collect. breadth.toml records the removals. Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
f5a904b7f2
commit
f63703a604
@@ -87,12 +87,6 @@ def verdicts(cpg: CpgResult, spec: ConfigSpec, *, function: str = "", file: str
|
||||
return []
|
||||
|
||||
|
||||
def verdict(cpg: CpgResult, spec: ConfigSpec, *, function: str = "", file: str = "") -> Verdict | None:
|
||||
"""The strongest single verdict, kept for callers that ask one question and want one answer."""
|
||||
answers = verdicts(cpg, spec, function=function, file=file)
|
||||
return answers[0] if answers else None
|
||||
|
||||
|
||||
def _location(row: Mapping[str, object], file: str) -> str:
|
||||
"""``path:line:function`` of the setting, when the region named a file.
|
||||
|
||||
@@ -156,4 +150,4 @@ def _settings(rows: object, spec: ConfigSpec, *, function: str) -> list[dict[str
|
||||
return sorted(kept, key=lambda row: (str(row["method"]), _line(row), str(row["setting"])))
|
||||
|
||||
|
||||
__all__ = ["request_params", "verdict", "verdicts"]
|
||||
__all__ = ["request_params", "verdicts"]
|
||||
|
||||
@@ -29,12 +29,10 @@ from typing import Any
|
||||
|
||||
from ..cpg.backend import CpgResult
|
||||
from ..redaction import redact_secrets
|
||||
from .config_value import verdict as config_verdict
|
||||
from .config_value import verdicts as config_verdicts
|
||||
from .dominance import verdict as dominance_verdict
|
||||
from .ladder import Rung, Verdict
|
||||
from .specs import ConfigSpec, DominanceSpec, TaintSpec
|
||||
from .taint import verdict as taint_verdict
|
||||
from .taint import verdicts as taint_verdicts
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -93,34 +91,11 @@ def _arbitrate_all(
|
||||
if isinstance(spec, ConfigSpec):
|
||||
return config_verdicts(cpg, spec, function=function, file=path)
|
||||
if isinstance(spec, DominanceSpec):
|
||||
answer = _arbitrate(cpg, spec, path=path, function=function, parameter_sources=parameter_sources, call_depth=call_depth)
|
||||
answer = dominance_verdict(cpg, spec, function=function, file=path)
|
||||
return [answer] if answer is not None else []
|
||||
return taint_verdicts(cpg, spec, function=function, file=path, parameter_sources=parameter_sources, call_depth=call_depth)
|
||||
|
||||
|
||||
def _arbitrate(
|
||||
cpg: CpgResult,
|
||||
spec: TaintSpec | DominanceSpec | ConfigSpec,
|
||||
*,
|
||||
path: str,
|
||||
function: str,
|
||||
parameter_sources: bool,
|
||||
call_depth: int,
|
||||
) -> Verdict | None:
|
||||
"""Route a family to the arbiter that can actually decide it.
|
||||
|
||||
Dispatching on the spec type rather than on a family name keeps this honest: a family without an arbiter
|
||||
cannot be silently routed to the wrong one. The pipeline first shipped taint-only, so access_control and
|
||||
config_secrets -- whose arbiters had worked since group 4 -- produced nothing, and every one of their
|
||||
pairs read as `both_silent` on the first end-to-end run.
|
||||
"""
|
||||
if isinstance(spec, DominanceSpec):
|
||||
return dominance_verdict(cpg, spec, function=function, file=path)
|
||||
if isinstance(spec, ConfigSpec):
|
||||
return config_verdict(cpg, spec, function=function, file=path)
|
||||
return taint_verdict(cpg, spec, function=function, file=path, parameter_sources=parameter_sources, call_depth=call_depth)
|
||||
|
||||
|
||||
def scan_region(
|
||||
cpg: CpgResult,
|
||||
spec: TaintSpec | DominanceSpec | ConfigSpec,
|
||||
|
||||
@@ -1,87 +1,9 @@
|
||||
"""Reporting that cannot flatter itself (model-grounded-detection, Req 9.3, 11.1).
|
||||
|
||||
Three disciplines survive from the deleted publish/scoring layer, and each is here because its absence caused
|
||||
a real error in this project's history rather than because it is tidy:
|
||||
|
||||
* **Per-slice rows, always.** A headline over mixed slices once hid that agent-written code scored 44.4%
|
||||
while the hand-written development slice scored 83.3%. `render` refuses to produce anything without rows.
|
||||
* **The overfitting gap beside the headline.** The distance between the development slice and the deciding
|
||||
real-world one is stated, never left for the reader to compute. When a slice is missing the gap is
|
||||
*absent*, not zero — reading a missing measurement as "no overfitting" is the flattering failure.
|
||||
* **The rung on every finding.** A finding's strength is what established it; omitting the rung invites a
|
||||
reader to treat a suspicion as a result.
|
||||
"""
|
||||
|
||||
"""Report model coverage for the scanned regions."""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
from dataclasses import dataclass
|
||||
|
||||
from ..findings import StaticFinding
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SliceRow:
|
||||
"""One slice's coverage. `covered` is pairs the model *split*, not pairs it flagged."""
|
||||
|
||||
slice: str
|
||||
pairs: int
|
||||
covered: int
|
||||
|
||||
@property
|
||||
def rate(self) -> float:
|
||||
return self.covered / self.pairs if self.pairs else 0.0
|
||||
|
||||
|
||||
def overfitting_gap(rows: Sequence[SliceRow], *, development: str = "", deciding: str = "") -> float | None:
|
||||
"""Development-slice coverage minus the deciding slice's, or ``None`` when either is absent.
|
||||
|
||||
``None`` rather than ``0.0`` on purpose: a slice that was never measured must not read as agreement
|
||||
between them.
|
||||
"""
|
||||
by_name = {row.slice: row for row in rows}
|
||||
if development not in by_name or deciding not in by_name:
|
||||
return None
|
||||
return by_name[development].rate - by_name[deciding].rate
|
||||
|
||||
|
||||
def render(rows: Sequence[SliceRow], *, development: str = "", deciding: str = "") -> str:
|
||||
"""The report. Raises when there are no per-slice rows, because a bare aggregate is the thing to prevent."""
|
||||
if not rows:
|
||||
raise ValueError("a report needs per-slice rows: there is no headline without them")
|
||||
|
||||
lines = ["## Model coverage", "", "| slice | pairs | covered | coverage |", "|---|---|---|---|"]
|
||||
for row in sorted(rows, key=lambda r: r.slice):
|
||||
lines.append(f"| {row.slice} | {row.pairs} | {row.covered} | {row.rate:.0%} |")
|
||||
|
||||
total_pairs = sum(row.pairs for row in rows)
|
||||
total_covered = sum(row.covered for row in rows)
|
||||
lines += [
|
||||
"",
|
||||
f"Across every slice: {total_covered}/{total_pairs} = {total_covered / total_pairs:.0%} — read the rows above it, not this line.",
|
||||
]
|
||||
|
||||
gap = overfitting_gap(rows, development=development, deciding=deciding)
|
||||
if gap is not None:
|
||||
lines += [
|
||||
"",
|
||||
f"**Overfitting gap: {gap:+.0%}** — `{development}` (development) minus `{deciding}`, which is the deciding real-world slice.",
|
||||
]
|
||||
elif development or deciding:
|
||||
lines += ["", f"**Overfitting gap: not measurable** — `{development}` or `{deciding}` is absent from this run."]
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
|
||||
def render_findings(findings: Sequence[StaticFinding]) -> str:
|
||||
"""Findings with the rung that established each one (Req 5.3)."""
|
||||
lines = ["## Findings", "", "| finding | location | rung | severity |", "|---|---|---|---|"]
|
||||
for finding in findings:
|
||||
where = f"{finding.path}:{finding.line}" if finding.line else finding.path
|
||||
lines.append(f"| {finding.title} | {where} | `{finding.rung}` | {finding.severity} |")
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
|
||||
__all__ = ["SliceRow", "overfitting_gap", "render", "render_findings"]
|
||||
__all__ = []
|
||||
|
||||
|
||||
def coverage_rows(regions: Sequence[object]) -> list[dict[str, object]]:
|
||||
|
||||
@@ -5,7 +5,7 @@ graph establishes it and the LLM's claim is no longer load-bearing. A path a san
|
||||
`CORROBORATED`: the flow is real, but whether the sanitizer is *sufficient* is a semantic judgement the model
|
||||
cannot make, and that residual is the LLM's — checked against this verdict rather than averaged over runs.
|
||||
|
||||
No path at all yields `None`, not a negative verdict: the caller reports `suspicion`. And a query that failed
|
||||
No path at all yields an empty list, not a negative verdict: the caller reports `suspicion`. And a query that failed
|
||||
yields `None` too, because "the engine could not decide" is not "there is no flow" — conflating them is how a
|
||||
missing engine would silently become a clean bill of health.
|
||||
"""
|
||||
@@ -69,37 +69,6 @@ def request_params(
|
||||
}
|
||||
|
||||
|
||||
def verdict(
|
||||
cpg: CpgResult, spec: TaintSpec, *, function: str = "", file: str = "", parameter_sources: bool = False, call_depth: int = 0
|
||||
) -> Verdict | None:
|
||||
"""The strongest verdict the taint query supports for ``spec`` in ``function``, or ``None`` to stay at suspicion.
|
||||
|
||||
``parameter_sources`` additionally treats the labeled function's own parameters as untrusted. That is right
|
||||
for a function-level pair, where the function boundary *is* the trust boundary, and wrong for a whole
|
||||
repository, where most parameters carry internal values — so it is off by default and the caller opts in.
|
||||
An ENTRY POINT is the case where both are true at once: its parameters are attacker input by definition,
|
||||
which is what the driver uses to decide.
|
||||
|
||||
``call_depth`` admits sinks that many call-graph levels below the labeled method. Zero keeps the old
|
||||
function-local question; above zero is what lets a handler's parameter reach a sink in another module,
|
||||
which is the shape VAmPI's SQL injection has and the shape a function-scoped question cannot see.
|
||||
"""
|
||||
# The strongest of the per-site verdicts, which is what this always meant. It used to run its own
|
||||
# selection over the union of flows, and two copies of "which flow decides" is the duplication this
|
||||
# module has been bitten by before: the per-site preference added to `verdicts` did not reach here, so
|
||||
# the two entry points disagreed about the same rows.
|
||||
#
|
||||
# A flow the graph cannot see a discharge for is the finding; a sanitized or BOUNDED one is a question.
|
||||
# libpng bounds `strcpy(outname+len, ".png")` with `(len = strlen(inname)) > 250` against a char[256],
|
||||
# and entailing it is a claim the code contradicts. But the bound reaches the sink through an `error`
|
||||
# flag rather than by dominating it, so the graph has not shown the copy is guarded either -- and a
|
||||
# bound is no proof of safety in any case, off-by-one being the classic way they fail. Corroborated is
|
||||
# the honest rung: the model cannot assert this, so the judge is asked instead of the site being
|
||||
# silenced.
|
||||
answers = verdicts(cpg, spec, function=function, file=file, parameter_sources=parameter_sources, call_depth=call_depth)
|
||||
return answers[0] if answers else None
|
||||
|
||||
|
||||
def _usable_flows(
|
||||
cpg: CpgResult,
|
||||
spec: TaintSpec,
|
||||
@@ -137,17 +106,7 @@ def verdicts(
|
||||
parameter_sources: bool = False,
|
||||
call_depth: int = 0,
|
||||
) -> list[Verdict]:
|
||||
"""One verdict per distinct SINK SITE, strongest first.
|
||||
|
||||
``verdict`` answers "what is the strongest thing to say about this region", which is the right question
|
||||
for a pair: one labelled function, one bug, one answer. It is the wrong question for a region that spans
|
||||
a file. A PHP file with an SQL injection on line 6 and a command injection on line 13 got ONE injection
|
||||
verdict, and `system` won it on flow length -- so the SQL injection went unreported, by construction
|
||||
rather than by any failure of the analysis.
|
||||
|
||||
The first element is exactly what ``verdict`` returns, so the pair path and the committed measurements
|
||||
that rest on it are unchanged.
|
||||
"""
|
||||
"""One verdict per distinct sink site, strongest first."""
|
||||
flows = _usable_flows(cpg, spec, function=function, file=file, parameter_sources=parameter_sources, call_depth=call_depth)
|
||||
if not flows:
|
||||
return []
|
||||
@@ -164,7 +123,7 @@ def verdicts(
|
||||
preferred = [flow for flow in site_flows if _from_modelled_source(flow, spec)] or list(site_flows)
|
||||
open_flows = [flow for flow in preferred if not flow["sanitized"] and not flow["bounded"]]
|
||||
chosen.append(min(open_flows or preferred, key=lambda flow: _rank(flow, spec)))
|
||||
# Same total order the single-verdict path uses, so `verdicts(...)[0] == verdict(...)`.
|
||||
# Sort strongest first, then by site for stable results.
|
||||
chosen.sort(key=lambda flow: (bool(flow["sanitized"]) or bool(flow["bounded"]), _rank(flow, spec)))
|
||||
return [_verdict_for(flow, spec) for flow in chosen]
|
||||
|
||||
@@ -309,4 +268,4 @@ def _location(flow: Mapping[str, object]) -> str:
|
||||
return f"{where}:{line}:{flow.get('sinkMethod') or '?'}"
|
||||
|
||||
|
||||
__all__ = ["request_params", "verdict"]
|
||||
__all__ = ["request_params", "verdicts"]
|
||||
|
||||
@@ -13,7 +13,6 @@ from __future__ import annotations
|
||||
import tomllib
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Literal
|
||||
|
||||
FAMILY_IDS = (
|
||||
"injection",
|
||||
@@ -28,7 +27,6 @@ FAMILY_IDS = (
|
||||
"unknown",
|
||||
)
|
||||
VERIFIER_KINDS = ("canary", "static", "none")
|
||||
Relation = Literal["same", "parent_child", "lateral", "fabricated"]
|
||||
DEFAULT_FAMILIES_PATH = Path(__file__).resolve().parents[1] / "ruleset" / "families.toml"
|
||||
|
||||
_FAMILY_FIELDS = frozenset({"id", "description", "cwes", "mechanisms", "verifier", "parent", "limits"})
|
||||
@@ -72,24 +70,6 @@ class FamilyTaxonomy:
|
||||
def family_of_mechanism(self, mechanism: str) -> Family | None:
|
||||
return next((family for family in self.families if mechanism in family.mechanisms), None)
|
||||
|
||||
@property
|
||||
def hierarchical(self) -> bool:
|
||||
"""True when some family declares a parent. While this is False, partial credit can never be earned."""
|
||||
return any(family.parent for family in self.families)
|
||||
|
||||
def related(self, left: str, right: str) -> Relation:
|
||||
"""How two family answers relate. Abstention is not partial credit: ``unknown`` against a real
|
||||
family is ``lateral``, never ``parent_child``."""
|
||||
known = {family.id: family for family in self.families}
|
||||
if left not in known or right not in known:
|
||||
return "fabricated"
|
||||
if left == right:
|
||||
return "same"
|
||||
if known[left].parent == right or known[right].parent == left:
|
||||
return "parent_child"
|
||||
return "lateral"
|
||||
|
||||
|
||||
def _load_families_uncached(path: Path | None = None, *, require_all: bool = True) -> FamilyTaxonomy:
|
||||
"""Load the taxonomy. ``require_all`` demands the full closed set in order (the shipped file);
|
||||
a scoped fixture may declare a subset, but never a family outside ``FAMILY_IDS``."""
|
||||
@@ -189,7 +169,6 @@ __all__ = [
|
||||
"FamiliesError",
|
||||
"Family",
|
||||
"FamilyTaxonomy",
|
||||
"Relation",
|
||||
"load_families",
|
||||
]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user