from __future__ import annotations import json from collections.abc import Collection from dataclasses import asdict, dataclass, replace from pathlib import Path from typing import TYPE_CHECKING, Protocol from .preprocess import FileTarget if TYPE_CHECKING: from .findings import StaticFinding class RankingError(ValueError): """Raised when ranker output does not match the expected schema.""" @dataclass(frozen=True) class RankingScore: path: str surface: int influence: int reachability: int priority: float rationale: str model_id: str | None static_boosts: list[str] class RankerClient(Protocol): def complete_json(self, *, model: str, messages: list[dict[str, str]], timeout_seconds: int = 60) -> object: raise NotImplementedError def rank_targets(targets: list[FileTarget], model_rankings: object | None = None, model_id: str | None = None) -> list[RankingScore]: model_by_path = parse_ranker_response(model_rankings, model_id) if model_rankings is not None else {} rankings: list[RankingScore] = [] for target in targets: heuristic = heuristic_rank(target) override = model_by_path.get(target.path) rankings.append(_merge_rankings(heuristic, override) if override is not None else heuristic) return sorted(rankings, key=lambda item: (-item.priority, item.path)) def rank_targets_with_model( targets: list[FileTarget], *, client: RankerClient, model: str, chunk_size: int = 150, timeout_seconds: int = 60, ) -> list[RankingScore]: if chunk_size < 1: raise RankingError("chunk_size must be at least 1") model_rankings: list[dict[str, object]] = [] for start in range(0, len(targets), chunk_size): chunk = targets[start : start + chunk_size] response = client.complete_json( model=model, messages=[ {"role": "system", "content": _ranker_system_prompt()}, {"role": "user", "content": json.dumps([_target_prompt_payload(target) for target in chunk], sort_keys=True)}, ], timeout_seconds=timeout_seconds, ) for ranking in parse_ranker_response(response, model).values(): model_rankings.append(asdict(ranking)) return rank_targets(targets, {"rankings": model_rankings}, model_id=model) def write_rankings(rankings: list[RankingScore], path: Path) -> None: path.parent.mkdir(parents=True, exist_ok=True) payload = {"rankings": [asdict(ranking) for ranking in rankings]} path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n") def heuristic_rank(target: FileTarget) -> RankingScore: tags = set(target.tags) surface = 1 influence = 1 reachability = 1 boosts: list[str] = [] if "memory_unsafe" in tags: surface = max(surface, 3) boosts.append("memory_unsafe") if "parser" in tags or "deserialization" in tags: surface = max(surface, 4) reachability = max(reachability, 3) boosts.append("input_parser") if "network_entry" in tags: reachability = max(reachability, 4) boosts.append("network_entry") if "auth_boundary" in tags: surface = max(surface, 3) reachability = max(reachability, 3) boosts.append("auth_boundary") if "crypto" in tags: influence = max(influence, 4) boosts.append("crypto") if "syscall_entry" in tags or "filesystem_entry" in tags: surface = max(surface, 3) boosts.append("privileged_boundary") if target.has_fuzz_entry_point or "fuzzable" in tags: surface = max(surface, 5) reachability = max(reachability, 4) boosts.append("fuzzable") if target.loc >= 500: influence = max(influence, 3) boosts.append("large_file") reachability_boost = _reachability_from_hints(target.reachability_hints) if reachability_boost: reachability = max(reachability, reachability_boost) boosts.append("entry_point_reachable") return RankingScore( path=target.path, surface=surface, influence=influence, reachability=reachability, priority=composite_priority(surface, influence, reachability), rationale=_heuristic_rationale(target, boosts), model_id=None, static_boosts=sorted(set(boosts)), ) def composite_priority(surface: int, influence: int, reachability: int) -> float: return round(_clamp_score(surface) * 0.5 + _clamp_score(influence) * 0.2 + _clamp_score(reachability) * 0.3, 2) def parse_ranker_response(payload: object, model_id: str | None) -> dict[str, RankingScore]: items = _ranking_items(payload) rankings: dict[str, RankingScore] = {} for item in items: if not isinstance(item, dict): raise RankingError("each ranking item must be an object") path = item.get("path") rationale = item.get("rationale", "model ranking") if not isinstance(path, str) or not path: raise RankingError("ranking item requires non-empty path") if not isinstance(rationale, str) or not rationale: raise RankingError(f"ranking item for {path} requires rationale") surface = _required_score(item, "surface", path) influence = _required_score(item, "influence", path) reachability = _required_score(item, "reachability", path) static_boosts = item.get("static_boosts", []) if not isinstance(static_boosts, list): raise RankingError(f"ranking item for {path} has invalid static_boosts") rankings[path] = RankingScore( path=path, surface=surface, influence=influence, reachability=reachability, priority=composite_priority(surface, influence, reachability), rationale=rationale, model_id=model_id, static_boosts=sorted(str(boost) for boost in static_boosts), ) return rankings def _ranking_items(payload: object) -> list[object]: if isinstance(payload, list): return payload if isinstance(payload, dict): rankings = payload.get("rankings") if isinstance(rankings, list): return rankings raise RankingError("ranker response must be a list or object with rankings list") def _required_score(item: dict[str, object], field: str, path: str) -> int: value = item.get(field) if not isinstance(value, int) or isinstance(value, bool) or value < 1 or value > 5: raise RankingError(f"ranking item for {path} requires integer {field} from 1 to 5") return value def _merge_rankings(heuristic: RankingScore, model: RankingScore) -> RankingScore: return RankingScore( path=heuristic.path, surface=max(heuristic.surface, model.surface), influence=max(heuristic.influence, model.influence), reachability=max(heuristic.reachability, model.reachability), priority=composite_priority( max(heuristic.surface, model.surface), max(heuristic.influence, model.influence), max(heuristic.reachability, model.reachability), ), rationale=model.rationale, model_id=model.model_id, static_boosts=sorted(set(heuristic.static_boosts) | set(model.static_boosts)), ) def _heuristic_rationale(target: FileTarget, boosts: list[str]) -> str: if boosts: return f"heuristic rank from tags: {', '.join(sorted(set(boosts)))}" return f"baseline rank for {target.language} file" def _clamp_score(value: int) -> int: return min(5, max(1, value)) def _ranker_system_prompt() -> str: return ( "Score each file for security review priority. Return JSON only with a rankings array. " "Each item must include path, surface, influence, reachability, rationale, and optional static_boosts. " "Scores must be integers from 1 to 5." ) def _target_prompt_payload(target: FileTarget) -> dict[str, object]: return { "path": target.path, "language": target.language, "loc": target.loc, "tags": target.tags, "has_fuzz_entry_point": target.has_fuzz_entry_point, "reachability_hints": target.reachability_hints, } def _reachability_from_hints(hints: list[dict[str, object]]) -> int: score = 0 for hint in hints: access_level = hint.get("access_level") kind = hint.get("kind") if access_level == "public": score = max(score, 5 if kind in {"route", "parser", "fuzz"} else 4) elif access_level in {"authenticated", "contract-only/callback"}: score = max(score, 4) elif access_level == "role-restricted": score = max(score, 3) elif access_level == "review-required" or access_level == "local-only": score = max(score, 2) return score # authorization-obligations Req 6.4: obligation findings rank by resource sensitivity and evidence label, and never above # a sandbox-proven finding in the same run. Every other finding keeps its priority. _OBLIGATION_LABEL_WEIGHT = {"declared_policy_violation": 0.9, "consistency_violation": 0.6, "function_local": 0.3} _OBLIGATION_SENSITIVITY_WEIGHT = {"high": 1.0, "medium": 0.7, "low": 0.4} _PROVEN_LEVELS = frozenset({"crash_reproduced", "exploit_demonstrated", "patch_validated", "root_cause_explained"}) def rank_obligations(findings: list[StaticFinding], *, proven_ids: Collection[str] = ()) -> list[StaticFinding]: """Rank obligation findings strictly below every proven finding in the run. Proof rungs live on verdicts, not on ``StaticFinding``, so the sandbox's proven finding ids are passed in; ``evidence_level`` is honoured as well.""" proven = [f.ranking_priority for f in findings if f.evidence_level in _PROVEN_LEVELS or f.finding_id in proven_ids] ceiling = min(proven) if proven else None ranked: list[StaticFinding] = [] for finding in findings: if not finding.finding_id.startswith("obligation:"): ranked.append(finding) continue label = next((tag.split(":", 1)[1] for tag in finding.tags if tag.startswith("obligation_evidence:")), "function_local") weight = _OBLIGATION_SENSITIVITY_WEIGHT.get(finding.severity, 0.7) * _OBLIGATION_LABEL_WEIGHT.get(label, 0.3) if ceiling is not None and weight >= ceiling: weight = max(ceiling * 0.99, 0.0) * _OBLIGATION_LABEL_WEIGHT.get(label, 0.3) / 0.9 # strictly below the lowest proven finding ranked.append(replace(finding, ranking_priority=round(weight, 4))) return ranked