Files
Open_Ultra_SAST/src/openultrasast/rank.py
T

266 lines
10 KiB
Python
Raw Normal View History

2026-06-17 21:13:51 +02:00
from __future__ import annotations
import json
from collections.abc import Collection
from dataclasses import asdict, dataclass, replace
2026-06-17 21:13:51 +02:00
from pathlib import Path
from typing import TYPE_CHECKING, Protocol
2026-06-17 21:13:51 +02:00
from .preprocess import FileTarget
if TYPE_CHECKING:
from .findings import StaticFinding
2026-06-17 21:13:51 +02:00
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")
2026-06-17 21:13:51 +02:00
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,
2026-06-17 21:13:51 +02:00
}
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)
2026-06-18 10:06:21 +02:00
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