feat(model): phase 2 of flow-aware-ranking -- spend the region budget by evidence, weights published

`Evidence.score` orders pairs within a tier by one table of weights (open sinks, cleansed sinks, local /
carried / near sources, entry, declared public), capped so volume stops counting, and zero for tier 0 so
the weights can rank but never manufacture. `ScanBudget.order_by_evidence` runs the evidence pass over
every region and cuts `max_regions` from each region's best (tier, score); `order_by_evidence()` is the
one ordering function, used by the scan and by `benchmarks/ranking/position.py --evidence`. Off by
default until the position benchmark says where the pinned CVEs land under it.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01T7XD7x5KonMdmGRkp63EQG
This commit is contained in:
Marius Ciepluch
2026-09-11 10:47:47 +02:00
co-authored by Claude Fable 5.1
parent 7ce0a30161
commit 60d13d7b6e
6 changed files with 295 additions and 43 deletions
+44
View File
@@ -21,6 +21,20 @@ TIER_CLEANSED = 2 # a source exists, but every sink call is bound or cleansed o
TIER_OPEN = 3 # a source exists and some sink call is computed and uncleansed: ask first
TIER_OPEN_PUBLIC = 4 # tier 3 and declared public, or a source on the same statement: ask very first
# The within-tier weights, published here because they are the whole of what "heuristic ranking" means in
# phase 2 and a reader should be able to see every one of them at once. They order, they never exclude.
WEIGHTS: dict[str, float] = {
"open_sink": 1.0, # per sink call that is computed, uncleansed and unbound, up to OPEN_SINK_CAP
"cleansed_sink": -0.5, # per sink call that is cleansed on the call or bound, up to CLEANSED_SINK_CAP
"source_local": 2.0, # a modelled source in the region's own body
"carried": 1.5, # stage one of the two-stage join says a field or dispatch value arrives tainted
"source_near": 1.0, # a modelled source within the arbiter's callDepth
"entry": 0.5, # parameters untrusted by contract
"access_declared_public": 1.0,
}
OPEN_SINK_CAP = 5
CLEANSED_SINK_CAP = 3
@dataclass(frozen=True)
class SinkEvidence:
@@ -77,6 +91,33 @@ class Evidence:
return TIER_OPEN_PUBLIC
return TIER_OPEN
@property
def score(self) -> float:
"""Order WITHIN a tier, by the published weights below. Never across tiers: the tier is the predicate.
A weighted sum of the same exact facts the tier is made of, so it can rank but cannot manufacture --
a pair with no sink scores nothing and stays tier 0 whatever its weights. Counts are capped because
a file-scope region with 346 sinks is not 346 times more likely to hold the bug than one with 5;
the cap is where the evidence stops saying more. Phase 2 of flow-aware-ranking.
"""
if self.tier == TIER_EXCLUDE:
return 0.0 # no sink in reach: nothing to weigh, whatever the sources say
w = WEIGHTS
return (
w["open_sink"] * min(len(self.open_sinks), OPEN_SINK_CAP)
+ w["cleansed_sink"] * min(sum(1 for s in self.sinks if s.cleansed_on_call or self.is_bound(s)), CLEANSED_SINK_CAP)
+ (w["source_local"] if self.source_local else 0.0)
+ (w["carried"] if self.carried else 0.0)
+ (w["source_near"] if self.source_near else 0.0)
+ (w["entry"] if self.entry else 0.0)
+ (w["access_declared_public"] if self.access_declared_public else 0.0)
)
@property
def order_key(self) -> tuple[int, float]:
"""Higher sorts first: ``sorted(..., key=lambda e: e.order_key, reverse=True)``."""
return (self.tier, self.score)
def evidence_from_rows(
rows: Sequence[Mapping[str, object]] | None,
@@ -139,11 +180,14 @@ def _as_int(value: object) -> int:
__all__ = [
"CLEANSED_SINK_CAP",
"OPEN_SINK_CAP",
"TIER_CLEANSED",
"TIER_EXCLUDE",
"TIER_NO_SOURCE",
"TIER_OPEN",
"TIER_OPEN_PUBLIC",
"WEIGHTS",
"Evidence",
"SinkEvidence",
"evidence_from_rows",
+108 -41
View File
@@ -26,10 +26,10 @@ from pathlib import Path
from typing import Any
from ..cpg.backend import CpgResult
from .evidence import TIER_EXCLUDE, evidence_from_rows
from .candidates import enumerate_candidates
from .config_value import request_params as config_params
from .dominance import request_params as dominance_params
from .evidence import TIER_EXCLUDE, Evidence, evidence_from_rows
from .ladder import Rung
from .pipeline import ModelFinding, scan_region
from .regions import MODULE_SCOPE, ScanRegion
@@ -75,6 +75,12 @@ class ScanBudget:
# language, under any dataflow model. Skipping it cannot lose a finding. It is the only tier that
# excludes, which is why it is the only one allowed to act before the ranker is measured.
prune_tier0: bool = True
# Phase 2 of flow-aware-ranking: spend the region budget by EVIDENCE, not by static rank. The evidence
# pass runs over `evidence_regions` regions in static order (0 = every region; it costs milliseconds a
# pair), each region takes its best pair's (tier, score), and `max_regions` is then cut from that
# order. Off by default until the position benchmark says where the pinned CVEs land under it.
order_by_evidence: bool = False
evidence_regions: int = 0
@dataclass(frozen=True)
@@ -185,50 +191,32 @@ def scan_repository(
}
)
# Phase 1: collect the whole scan's questions, for at most `max_regions` regions. Nothing is asked yet.
work: list[tuple[str, ScanRegion, ArbiterSpec]] = []
for region in ordered_regions[: limits.max_regions]:
for family in region.families:
spec = _spec_for(family, region.language)
if spec is None:
continue
work.append((f"{len(work)}", region, spec))
# Phase 1b: the evidence tier of every taint pair, before any dataflow is asked. Same query, `evidenceOnly`,
# no `reachableByFlows`. Recorded on the result and NOT used to reorder or skip anything -- that is a
# later phase, and it must be measured against this record before it is allowed to change behaviour.
# no `reachableByFlows`. Phase 0 recorded it; phase 1 prunes tier 0 on it; phase 2 spends the region
# budget by it. Under phase 2 the pass runs over MORE regions than the budget, which is the point.
tiers: list[tuple[str, str, str, int]] = []
tier_by_rid: dict[str, int] = {}
per_kind: dict[str, float] = {}
evidence_by_pair: dict[tuple[str, str, str], Evidence] = {}
if limits.tiering and callable(getattr(cpg, "run_batch", None)):
grouped_taint = _grouped(work, hook_callbacks=hooks).get("taint", {})
if grouped_taint:
evidence_requests = {rid: {**dict(params), "evidenceOnly": "true"} for rid, params in grouped_taint.items()}
tier_started = time.monotonic()
answered_evidence = cpg.run_batch("taint", evidence_requests)
per_kind["evidence"] = round(time.monotonic() - tier_started, 2)
if answered_evidence is None:
degradations.append({"stage": "model", "reason": "query_failed", "kind": "evidence", "requests": len(evidence_requests)})
else:
answered_evidence.pop("__census__", None)
by_rid = {rid: (region, spec) for rid, region, spec in work}
for rid, rows in answered_evidence.items():
pair = by_rid.get(rid)
if pair is None:
continue
region, spec = pair
evidence = evidence_from_rows(
rows,
entry=_is_entry_point(region),
# Rank 1.0 is the public tier. Provenance -- declared by a route contract versus
# inferred from an absent decorator -- is not yet carried on ScanRegion, so this is
# the proxy phase 0 has; the brief names it as the field to thread through next.
access_declared_public=region.rank >= 1.0,
bound_names=getattr(spec, "safe_shape_sinks", ()),
)
if evidence is not None:
tiers.append((region.path, region.function or "", getattr(spec, "family", ""), evidence.tier))
tier_by_rid[rid] = evidence.tier
candidates = (
ordered_regions[: (limits.evidence_regions or len(ordered_regions))]
if limits.order_by_evidence
else ordered_regions[: limits.max_regions]
)
tier_started = time.monotonic()
evidence_by_pair, failed = _evidence_pass(cpg, _collect(candidates), hooks)
per_kind["evidence"] = round(time.monotonic() - tier_started, 2)
if failed:
degradations.append({"stage": "model", "reason": "query_failed", "kind": "evidence", "requests": failed})
tiers = [(path, function, family, evidence.tier) for (path, function, family), evidence in evidence_by_pair.items()]
if limits.order_by_evidence and evidence_by_pair:
ordered_regions = order_by_evidence(ordered_regions, evidence_by_pair)
# Phase 1: collect the whole scan's questions, for at most `max_regions` regions. Nothing is asked yet.
work = _collect(ordered_regions[: limits.max_regions])
tier_by_rid = {
rid: evidence_by_pair[_pair_key(region, spec)].tier for rid, region, spec in work if _pair_key(region, spec) in evidence_by_pair
}
# Phase 2: ONE invocation per query kind. JVM startup dominates a repository scan -- a call per region
# per family put a ten-line file at four minutes and a thousand regions at roughly fifty hours -- while
@@ -362,6 +350,85 @@ def scan_repository(
)
def _pair_key(region: ScanRegion, spec: ArbiterSpec) -> tuple[str, str, str]:
return (region.path, region.function or "", getattr(spec, "family", ""))
def _collect(regions: Sequence[ScanRegion]) -> list[tuple[str, ScanRegion, ArbiterSpec]]:
"""Every (region, family) question with an arbiter, in the order given, with batch-local ids."""
work: list[tuple[str, ScanRegion, ArbiterSpec]] = []
for region in regions:
for family in region.families:
spec = _spec_for(family, region.language)
if spec is None:
continue
work.append((f"{len(work)}", region, spec))
return work
def _evidence_pass(
cpg: Any, work: Sequence[tuple[str, ScanRegion, ArbiterSpec]], hooks: str
) -> tuple[dict[tuple[str, str, str], Evidence], int]:
"""The evidence vector of every taint pair in ``work``, keyed by (path, function, family).
Returns the vectors and the number of requests the engine did not answer (0 when it answered), so a
failed pass is recorded rather than read as "no evidence anywhere".
"""
grouped_taint = _grouped(list(work), hook_callbacks=hooks).get("taint", {})
if not grouped_taint:
return {}, 0
requests = {rid: {**dict(params), "evidenceOnly": "true"} for rid, params in grouped_taint.items()}
answered = cpg.run_batch("taint", requests)
if answered is None:
return {}, len(requests)
answered.pop("__census__", None)
by_rid = {rid: (region, spec) for rid, region, spec in work}
out: dict[tuple[str, str, str], Evidence] = {}
for rid, rows in answered.items():
pair = by_rid.get(rid)
if pair is None:
continue
region, spec = pair
evidence = evidence_from_rows(
rows,
entry=_is_entry_point(region),
# Rank 1.0 is the public tier. Provenance -- declared by a route contract versus inferred from
# an absent decorator -- is not yet carried on ScanRegion, so this is the proxy phase 0 has;
# the brief names it as the field to thread through next.
access_declared_public=region.rank >= 1.0,
bound_names=getattr(spec, "safe_shape_sinks", ()),
)
if evidence is not None:
out[_pair_key(region, spec)] = evidence
return out, 0
def order_by_evidence(regions: Sequence[ScanRegion], evidence: Mapping[tuple[str, str, str], Evidence]) -> list[ScanRegion]:
"""Regions by their best pair's (tier, score), then by the static order they arrived in.
ONE definition, used by the scan to spend its budget and by `benchmarks/ranking/position.py` to say
where a pinned CVE lands under it; if they drifted, the benchmark would measure the wrong thing. A
region with no evidence at all -- no taint family, or a pass that failed -- sorts after every region
that has some, in static order: the scan does not know it is safe, it knows nothing about it.
"""
best: dict[tuple[str, str], tuple[int, float]] = {}
for (path, function, _family), vector in evidence.items():
key = (path, function)
best[key] = max(best.get(key, (-1, 0.0)), vector.order_key)
indexed = list(enumerate(regions))
return [
region
for _, region in sorted(
indexed,
key=lambda item: (
-best.get((item[1].path, item[1].function or ""), (-1, 0.0))[0],
-best.get((item[1].path, item[1].function or ""), (-1, 0.0))[1],
item[0],
),
)
]
def _dispose(cpg: object) -> None:
"""Release a built CPG's scratch tree. Never raises: cleanup must not turn a good scan into a failure."""
cleanup = getattr(cpg, "cleanup", None)