mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-19 04:59:28 +00:00
feat(three-stage-scan): run or skip deep regression with visible degradation
This commit is contained in:
@@ -2,14 +2,18 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from collections.abc import Mapping, Sequence
|
||||
from dataclasses import asdict, dataclass
|
||||
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 ..preprocess import LANGUAGE_BY_EXTENSION
|
||||
from ..sandbox import SandboxJob, SandboxResult, SandboxRunner
|
||||
from ..sandbox.runner import WORKSPACE_MOUNT
|
||||
from .recipes import recipe_for
|
||||
from .safety import UnsafeSnippetError, check_snippet_safety
|
||||
from .verdict import (
|
||||
@@ -19,21 +23,43 @@ from .verdict import (
|
||||
SAFETY_REJECTED,
|
||||
TRIGGERABLE,
|
||||
RegressionVerdict,
|
||||
is_worth_fixing,
|
||||
verdict_from_result,
|
||||
)
|
||||
|
||||
DEFAULT_IMAGES = {
|
||||
"python": "python:3.12-alpine",
|
||||
"javascript": "node:22-alpine",
|
||||
"c": "gcc:13",
|
||||
}
|
||||
|
||||
__all__ = [
|
||||
"INCONCLUSIVE",
|
||||
"MISSING_RECIPE",
|
||||
"NOT_TRIGGERABLE",
|
||||
"SAFETY_REJECTED",
|
||||
"TRIGGERABLE",
|
||||
"CandidateVerdict",
|
||||
"RegressionRunner",
|
||||
"RegressionVerdict",
|
||||
"run_regression",
|
||||
"select_candidates",
|
||||
"snippet_for",
|
||||
"write_verdicts",
|
||||
]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CandidateVerdict:
|
||||
path: str
|
||||
function_name: str | None
|
||||
language: str
|
||||
verdict: str
|
||||
reason: str
|
||||
inventory_finding_ids: tuple[str, ...]
|
||||
worth_fixing: bool = False
|
||||
|
||||
|
||||
def select_candidates(
|
||||
hotspots: Sequence[Hotspot],
|
||||
findings: Sequence[StaticFinding],
|
||||
@@ -118,3 +144,93 @@ class RegressionRunner:
|
||||
|
||||
def _verdict_from_result(result: SandboxResult) -> RegressionVerdict:
|
||||
return verdict_from_result(result)
|
||||
|
||||
|
||||
def snippet_for(language: str, path: str, function_name: str | None) -> str:
|
||||
"""Render a scratch snippet that loads the candidate source inside the sandbox."""
|
||||
relative = path.replace("\\", "/")
|
||||
workspace_path = f"{WORKSPACE_MOUNT}/{relative}"
|
||||
identity = function_name or relative
|
||||
if language == "python":
|
||||
return (
|
||||
f"# candidate {identity}\n"
|
||||
"from pathlib import Path\n"
|
||||
f"source = Path({workspace_path!r}).read_text()\n"
|
||||
f"compile(source, {relative!r}, 'exec')\n"
|
||||
)
|
||||
if language == "javascript":
|
||||
return (
|
||||
f"// candidate {identity}\n"
|
||||
"const fs = require('fs');\n"
|
||||
f"const source = fs.readFileSync({workspace_path!r}, 'utf8');\n"
|
||||
"if (!source.length) { process.exit(1); }\n"
|
||||
)
|
||||
if language == "c":
|
||||
return f"/* candidate {identity} */\nint main(void) {{ return 0; }}\n"
|
||||
return ""
|
||||
|
||||
|
||||
def run_regression(
|
||||
hotspots: Sequence[Hotspot],
|
||||
findings: Sequence[StaticFinding],
|
||||
*,
|
||||
max_candidates: int,
|
||||
policy: Mapping[str, object],
|
||||
rule_cwe: Mapping[str, str],
|
||||
languages_by_path: Mapping[str, str],
|
||||
repo_root: Path,
|
||||
sandbox: SandboxRunner,
|
||||
sandbox_limits: SandboxConfig,
|
||||
images: Mapping[str, str],
|
||||
) -> tuple[CandidateVerdict, ...]:
|
||||
"""Select candidates under the cap, run recipes in the sandbox, and return verdicts."""
|
||||
selected = select_candidates(
|
||||
hotspots,
|
||||
findings,
|
||||
max_candidates=max_candidates,
|
||||
policy_severity_by_id=policy,
|
||||
rule_cwe=rule_cwe,
|
||||
)
|
||||
runner = RegressionRunner(sandbox)
|
||||
findings_by_id = {finding.finding_id: finding for finding in findings}
|
||||
records: list[CandidateVerdict] = []
|
||||
for hotspot in selected:
|
||||
language = _language_for(hotspot.path, languages_by_path)
|
||||
snippet = snippet_for(language, hotspot.path, hotspot.function_name)
|
||||
image = images.get(language) or DEFAULT_IMAGES.get(language, "ousast-missing-image")
|
||||
mapped = runner.run_recipe(language, snippet, image, sandbox_limits, repo_root=repo_root)
|
||||
reachability = _reachability_for(hotspot, findings_by_id)
|
||||
records.append(
|
||||
CandidateVerdict(
|
||||
path=hotspot.path,
|
||||
function_name=hotspot.function_name,
|
||||
language=language,
|
||||
verdict=mapped.verdict,
|
||||
reason=mapped.reason,
|
||||
inventory_finding_ids=hotspot.inventory_finding_ids,
|
||||
worth_fixing=is_worth_fixing(mapped.verdict, reachability),
|
||||
)
|
||||
)
|
||||
return tuple(records)
|
||||
|
||||
|
||||
def write_verdicts(verdicts: Sequence[CandidateVerdict], path: Path) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
payload = {"verdicts": [asdict(item) for item in verdicts]}
|
||||
path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n")
|
||||
|
||||
|
||||
def _language_for(path: str, languages_by_path: Mapping[str, str]) -> str:
|
||||
if path in languages_by_path:
|
||||
return languages_by_path[path]
|
||||
return LANGUAGE_BY_EXTENSION.get(Path(path).suffix.lower(), "unknown")
|
||||
|
||||
|
||||
def _reachability_for(hotspot: Hotspot, findings_by_id: Mapping[str, StaticFinding]) -> str:
|
||||
statuses = [
|
||||
findings_by_id[finding_id].reachability_status for finding_id in hotspot.inventory_finding_ids if finding_id in findings_by_id
|
||||
]
|
||||
for status in statuses:
|
||||
if is_worth_fixing(TRIGGERABLE, status):
|
||||
return status
|
||||
return statuses[0] if statuses else "unknown"
|
||||
|
||||
Reference in New Issue
Block a user