mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-19 21:19:28 +00:00
121 lines
3.9 KiB
Python
121 lines
3.9 KiB
Python
"""Candidate selection and safety-gated regression execution."""
|
||
|
||
from __future__ import annotations
|
||
|
||
from collections.abc import Mapping, Sequence
|
||
from pathlib import Path
|
||
|
||
from ..complexity.ledger import hotspot_key, select_forced_candidates
|
||
from ..complexity.map import Hotspot
|
||
from ..config import SandboxConfig
|
||
from ..findings import StaticFinding
|
||
from ..sandbox import SandboxJob, SandboxResult, SandboxRunner
|
||
from .recipes import recipe_for
|
||
from .safety import UnsafeSnippetError, check_snippet_safety
|
||
from .verdict import (
|
||
INCONCLUSIVE,
|
||
MISSING_RECIPE,
|
||
NOT_TRIGGERABLE,
|
||
SAFETY_REJECTED,
|
||
TRIGGERABLE,
|
||
RegressionVerdict,
|
||
verdict_from_result,
|
||
)
|
||
|
||
__all__ = [
|
||
"INCONCLUSIVE",
|
||
"MISSING_RECIPE",
|
||
"NOT_TRIGGERABLE",
|
||
"SAFETY_REJECTED",
|
||
"TRIGGERABLE",
|
||
"RegressionRunner",
|
||
"RegressionVerdict",
|
||
"select_candidates",
|
||
]
|
||
|
||
|
||
def select_candidates(
|
||
hotspots: Sequence[Hotspot],
|
||
findings: Sequence[StaticFinding],
|
||
*,
|
||
max_candidates: int,
|
||
policy_severity_by_id: Mapping[str, object] | None = None,
|
||
rule_cwe: Mapping[str, str] | None = None,
|
||
) -> tuple[Hotspot, ...]:
|
||
"""Select min(max_candidates, ranked hotspots) ∪ sev-5 reachable inventory.
|
||
|
||
The cap applies to the ranked hotspot slice. Forced policy-severity-5
|
||
reachable (or inferred-file-surface) inventory stays in the set even when a
|
||
ledger overlay demoted its hotspot out of that slice.
|
||
"""
|
||
ranked = tuple(sorted(hotspots, key=lambda item: (-item.score, item.path, item.function_name or "")))
|
||
selected = list(ranked[: max(max_candidates, 0)])
|
||
seen = {hotspot_key(item.path, item.function_name) for item in selected}
|
||
extras: list[Hotspot] = []
|
||
for finding in select_forced_candidates(findings, policy_severity_by_id or {}, rule_cwe=rule_cwe):
|
||
hotspot = _hotspot_for_finding(finding, ranked) or _hotspot_from_forced_finding(finding)
|
||
key = hotspot_key(hotspot.path, hotspot.function_name)
|
||
if key in seen:
|
||
continue
|
||
seen.add(key)
|
||
extras.append(hotspot)
|
||
return tuple(selected + extras)
|
||
|
||
|
||
def _hotspot_for_finding(finding: StaticFinding, hotspots: Sequence[Hotspot]) -> Hotspot | None:
|
||
for hotspot in hotspots:
|
||
if finding.finding_id in hotspot.inventory_finding_ids:
|
||
return hotspot
|
||
key = hotspot_key(finding.path, finding.function_name)
|
||
for hotspot in hotspots:
|
||
if hotspot_key(hotspot.path, hotspot.function_name) == key:
|
||
return hotspot
|
||
return None
|
||
|
||
|
||
def _hotspot_from_forced_finding(finding: StaticFinding) -> Hotspot:
|
||
return Hotspot(
|
||
path=finding.path,
|
||
function_name=finding.function_name,
|
||
score=0.0,
|
||
band="low",
|
||
signals={},
|
||
rationale="forced reachable severity-5 inventory",
|
||
test_hint=None,
|
||
inventory_finding_ids=(finding.finding_id,),
|
||
)
|
||
|
||
|
||
class RegressionRunner:
|
||
"""Run a snippet in the sandbox only after the structural safety check passes."""
|
||
|
||
def __init__(self, sandbox: SandboxRunner) -> None:
|
||
self._sandbox = sandbox
|
||
|
||
def run_recipe(
|
||
self,
|
||
language: str,
|
||
snippet: str,
|
||
image: str,
|
||
sandbox_limits: SandboxConfig,
|
||
*,
|
||
repo_root: Path,
|
||
) -> RegressionVerdict:
|
||
job = recipe_for(language, snippet, image, sandbox_limits, repo_root=repo_root)
|
||
if job is None:
|
||
return verdict_from_result(recipe_missing=True)
|
||
return self.run_snippet(snippet, job)
|
||
|
||
def run_snippet(self, snippet: str, job: SandboxJob | None) -> RegressionVerdict:
|
||
if job is None:
|
||
return verdict_from_result(recipe_missing=True)
|
||
try:
|
||
check_snippet_safety(snippet)
|
||
except UnsafeSnippetError:
|
||
return verdict_from_result(safety_rejected=True)
|
||
return _verdict_from_result(self._sandbox.run(job))
|
||
|
||
|
||
def _verdict_from_result(result: SandboxResult) -> RegressionVerdict:
|
||
return verdict_from_result(result)
|