mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-16 19:59:26 +00:00
feat(three-stage-scan): map sandbox outcomes to worth-fixing verdicts
This commit is contained in:
@@ -1,17 +1,33 @@
|
||||
"""Isolated regression snippets and verdicts."""
|
||||
|
||||
from .candidate import INCONCLUSIVE, MISSING_RECIPE, SAFETY_REJECTED, RegressionRunner, RegressionVerdict, select_candidates
|
||||
from .candidate import RegressionRunner, select_candidates
|
||||
from .recipes import recipe_for
|
||||
from .safety import UnsafeSnippetError, check_snippet_safety
|
||||
from .verdict import (
|
||||
ALREADY_COVERED,
|
||||
INCONCLUSIVE,
|
||||
MISSING_RECIPE,
|
||||
NOT_TRIGGERABLE,
|
||||
SAFETY_REJECTED,
|
||||
TRIGGERABLE,
|
||||
RegressionVerdict,
|
||||
is_worth_fixing,
|
||||
verdict_from_result,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"ALREADY_COVERED",
|
||||
"INCONCLUSIVE",
|
||||
"MISSING_RECIPE",
|
||||
"NOT_TRIGGERABLE",
|
||||
"SAFETY_REJECTED",
|
||||
"TRIGGERABLE",
|
||||
"RegressionRunner",
|
||||
"RegressionVerdict",
|
||||
"UnsafeSnippetError",
|
||||
"check_snippet_safety",
|
||||
"is_worth_fixing",
|
||||
"recipe_for",
|
||||
"select_candidates",
|
||||
"verdict_from_result",
|
||||
]
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping, Sequence
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
from ..complexity.ledger import hotspot_key, select_forced_candidates
|
||||
@@ -13,12 +12,26 @@ 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,
|
||||
)
|
||||
|
||||
INCONCLUSIVE = "inconclusive"
|
||||
SAFETY_REJECTED = "safety_rejected"
|
||||
MISSING_RECIPE = "missing_recipe"
|
||||
TRIGGERABLE = "triggerable"
|
||||
NOT_TRIGGERABLE = "not_triggerable"
|
||||
__all__ = [
|
||||
"INCONCLUSIVE",
|
||||
"MISSING_RECIPE",
|
||||
"NOT_TRIGGERABLE",
|
||||
"SAFETY_REJECTED",
|
||||
"TRIGGERABLE",
|
||||
"RegressionRunner",
|
||||
"RegressionVerdict",
|
||||
"select_candidates",
|
||||
]
|
||||
|
||||
|
||||
def select_candidates(
|
||||
@@ -73,12 +86,6 @@ def _hotspot_from_forced_finding(finding: StaticFinding) -> Hotspot:
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RegressionVerdict:
|
||||
verdict: str
|
||||
reason: str = ""
|
||||
|
||||
|
||||
class RegressionRunner:
|
||||
"""Run a snippet in the sandbox only after the structural safety check passes."""
|
||||
|
||||
@@ -96,22 +103,18 @@ class RegressionRunner:
|
||||
) -> RegressionVerdict:
|
||||
job = recipe_for(language, snippet, image, sandbox_limits, repo_root=repo_root)
|
||||
if job is None:
|
||||
return RegressionVerdict(verdict=INCONCLUSIVE, reason=MISSING_RECIPE)
|
||||
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 RegressionVerdict(verdict=INCONCLUSIVE, reason=MISSING_RECIPE)
|
||||
return verdict_from_result(recipe_missing=True)
|
||||
try:
|
||||
check_snippet_safety(snippet)
|
||||
except UnsafeSnippetError:
|
||||
return RegressionVerdict(verdict=INCONCLUSIVE, reason=SAFETY_REJECTED)
|
||||
return verdict_from_result(safety_rejected=True)
|
||||
return _verdict_from_result(self._sandbox.run(job))
|
||||
|
||||
|
||||
def _verdict_from_result(result: SandboxResult) -> RegressionVerdict:
|
||||
if result.timed_out:
|
||||
return RegressionVerdict(verdict=INCONCLUSIVE, reason="timeout")
|
||||
if result.exit_code != 0:
|
||||
return RegressionVerdict(verdict=TRIGGERABLE, reason="nonzero_exit")
|
||||
return RegressionVerdict(verdict=NOT_TRIGGERABLE, reason="exit_zero")
|
||||
return verdict_from_result(result)
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
"""Map sandbox outcomes to verdicts and the worth-fixing gate."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from ..sandbox import SandboxResult
|
||||
|
||||
TRIGGERABLE = "triggerable"
|
||||
NOT_TRIGGERABLE = "not_triggerable"
|
||||
ALREADY_COVERED = "already_covered"
|
||||
INCONCLUSIVE = "inconclusive"
|
||||
|
||||
MISSING_RECIPE = "missing_recipe"
|
||||
SAFETY_REJECTED = "safety_rejected"
|
||||
SANDBOX_UNAVAILABLE = "sandbox_unavailable"
|
||||
TIMEOUT = "timeout"
|
||||
COVERED_TEST_PASSED = "covered_test_passed"
|
||||
ASSERTION = "assertion"
|
||||
SANITIZER_ABORT = "sanitizer_abort"
|
||||
CRASH = "crash"
|
||||
NONZERO_EXIT = "nonzero_exit"
|
||||
EXIT_ZERO = "exit_zero"
|
||||
|
||||
WORTH_FIXING_REACHABILITY = frozenset({"reachable", "inferred-file-surface"})
|
||||
|
||||
_SANITIZER_MARKERS = (
|
||||
"AddressSanitizer",
|
||||
"UndefinedBehaviorSanitizer",
|
||||
"ThreadSanitizer",
|
||||
"MemorySanitizer",
|
||||
"LeakSanitizer",
|
||||
)
|
||||
_ASSERTION_MARKERS = (
|
||||
"AssertionError",
|
||||
"Assertion failed",
|
||||
"assertion failed",
|
||||
"assert failed",
|
||||
)
|
||||
_CRASH_MARKERS = (
|
||||
"Fatal Python error",
|
||||
"Segmentation fault",
|
||||
"SIGSEGV",
|
||||
"SIGABRT",
|
||||
"Aborted",
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RegressionVerdict:
|
||||
verdict: str
|
||||
reason: str = ""
|
||||
|
||||
|
||||
def is_worth_fixing(verdict: str, reachability: str) -> bool:
|
||||
"""True iff the candidate is triggerable on a reachable or inferred-file-surface finding."""
|
||||
return verdict == TRIGGERABLE and reachability in WORTH_FIXING_REACHABILITY
|
||||
|
||||
|
||||
def verdict_from_result(
|
||||
result: SandboxResult | None = None,
|
||||
*,
|
||||
recipe_missing: bool = False,
|
||||
sandbox_missing: bool = False,
|
||||
safety_rejected: bool = False,
|
||||
covering_test: bool = False,
|
||||
) -> RegressionVerdict:
|
||||
"""Map a sandbox observation onto one of the four regression verdicts."""
|
||||
if recipe_missing:
|
||||
return RegressionVerdict(verdict=INCONCLUSIVE, reason=MISSING_RECIPE)
|
||||
if sandbox_missing:
|
||||
return RegressionVerdict(verdict=INCONCLUSIVE, reason=SANDBOX_UNAVAILABLE)
|
||||
if safety_rejected:
|
||||
return RegressionVerdict(verdict=INCONCLUSIVE, reason=SAFETY_REJECTED)
|
||||
if result is None:
|
||||
return RegressionVerdict(verdict=INCONCLUSIVE, reason=SANDBOX_UNAVAILABLE)
|
||||
if result.timed_out:
|
||||
return RegressionVerdict(verdict=INCONCLUSIVE, reason=TIMEOUT)
|
||||
if covering_test and result.exit_code == 0:
|
||||
return RegressionVerdict(verdict=ALREADY_COVERED, reason=COVERED_TEST_PASSED)
|
||||
if result.exit_code != 0:
|
||||
return RegressionVerdict(verdict=TRIGGERABLE, reason=_nonzero_reason(result))
|
||||
return RegressionVerdict(verdict=NOT_TRIGGERABLE, reason=EXIT_ZERO)
|
||||
|
||||
|
||||
def _nonzero_reason(result: SandboxResult) -> str:
|
||||
text = f"{result.stdout}\n{result.stderr}".lower()
|
||||
if any(marker.lower() in text for marker in _SANITIZER_MARKERS):
|
||||
return SANITIZER_ABORT
|
||||
if any(marker.lower() in text for marker in _ASSERTION_MARKERS):
|
||||
return ASSERTION
|
||||
if any(marker.lower() in text for marker in _CRASH_MARKERS):
|
||||
return CRASH
|
||||
return NONZERO_EXIT
|
||||
Reference in New Issue
Block a user