mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-17 20:19:29 +00:00
feat(three-stage-scan): add language recipes with inconclusive fallback
This commit is contained in:
@@ -110,7 +110,7 @@ This plan ships the three-stage scan without rebuilding CWE policy or the regex
|
|||||||
- _Boundary: RegressionRunner_
|
- _Boundary: RegressionRunner_
|
||||||
- _Depends: 2.4_
|
- _Depends: 2.4_
|
||||||
|
|
||||||
- [ ] 5.2 (P) Add language recipes with inconclusive fallback
|
- [x] 5.2 (P) Add language recipes with inconclusive fallback
|
||||||
- Python and JavaScript recipes run a scratch snippet with the repo on the language path; C compiles and runs when the image has a compiler; a missing recipe yields `inconclusive` rather than a crash.
|
- Python and JavaScript recipes run a scratch snippet with the repo on the language path; C compiles and runs when the image has a compiler; a missing recipe yields `inconclusive` rather than a crash.
|
||||||
- Observable completion: Python and JavaScript recipes produce a job; an unknown language returns inconclusive without calling the runner.
|
- Observable completion: Python and JavaScript recipes produce a job; an unknown language returns inconclusive without calling the runner.
|
||||||
- _Requirements: 5.1, 5.6_
|
- _Requirements: 5.1, 5.6_
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
"""Isolated regression snippets and verdicts."""
|
"""Isolated regression snippets and verdicts."""
|
||||||
|
|
||||||
from .candidate import INCONCLUSIVE, SAFETY_REJECTED, RegressionRunner, RegressionVerdict, select_candidates
|
from .candidate import INCONCLUSIVE, MISSING_RECIPE, SAFETY_REJECTED, RegressionRunner, RegressionVerdict, select_candidates
|
||||||
|
from .recipes import recipe_for
|
||||||
from .safety import UnsafeSnippetError, check_snippet_safety
|
from .safety import UnsafeSnippetError, check_snippet_safety
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"INCONCLUSIVE",
|
"INCONCLUSIVE",
|
||||||
|
"MISSING_RECIPE",
|
||||||
"SAFETY_REJECTED",
|
"SAFETY_REJECTED",
|
||||||
"RegressionRunner",
|
"RegressionRunner",
|
||||||
"RegressionVerdict",
|
"RegressionVerdict",
|
||||||
"UnsafeSnippetError",
|
"UnsafeSnippetError",
|
||||||
"check_snippet_safety",
|
"check_snippet_safety",
|
||||||
|
"recipe_for",
|
||||||
"select_candidates",
|
"select_candidates",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -4,15 +4,19 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from collections.abc import Mapping, Sequence
|
from collections.abc import Mapping, Sequence
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from ..complexity.ledger import hotspot_key, select_forced_candidates
|
from ..complexity.ledger import hotspot_key, select_forced_candidates
|
||||||
from ..complexity.map import Hotspot
|
from ..complexity.map import Hotspot
|
||||||
|
from ..config import SandboxConfig
|
||||||
from ..findings import StaticFinding
|
from ..findings import StaticFinding
|
||||||
from ..sandbox import SandboxJob, SandboxResult, SandboxRunner
|
from ..sandbox import SandboxJob, SandboxResult, SandboxRunner
|
||||||
|
from .recipes import recipe_for
|
||||||
from .safety import UnsafeSnippetError, check_snippet_safety
|
from .safety import UnsafeSnippetError, check_snippet_safety
|
||||||
|
|
||||||
INCONCLUSIVE = "inconclusive"
|
INCONCLUSIVE = "inconclusive"
|
||||||
SAFETY_REJECTED = "safety_rejected"
|
SAFETY_REJECTED = "safety_rejected"
|
||||||
|
MISSING_RECIPE = "missing_recipe"
|
||||||
TRIGGERABLE = "triggerable"
|
TRIGGERABLE = "triggerable"
|
||||||
NOT_TRIGGERABLE = "not_triggerable"
|
NOT_TRIGGERABLE = "not_triggerable"
|
||||||
|
|
||||||
@@ -81,7 +85,23 @@ class RegressionRunner:
|
|||||||
def __init__(self, sandbox: SandboxRunner) -> None:
|
def __init__(self, sandbox: SandboxRunner) -> None:
|
||||||
self._sandbox = sandbox
|
self._sandbox = sandbox
|
||||||
|
|
||||||
def run_snippet(self, snippet: str, job: SandboxJob) -> RegressionVerdict:
|
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 RegressionVerdict(verdict=INCONCLUSIVE, reason=MISSING_RECIPE)
|
||||||
|
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)
|
||||||
try:
|
try:
|
||||||
check_snippet_safety(snippet)
|
check_snippet_safety(snippet)
|
||||||
except UnsafeSnippetError:
|
except UnsafeSnippetError:
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
"""Language command recipes for isolated regression snippets."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from ..config import SandboxConfig
|
||||||
|
from ..sandbox.runner import SCRATCH_MOUNT, WORKSPACE_MOUNT, SandboxJob
|
||||||
|
|
||||||
|
_PYTHON_FILE = "case.py"
|
||||||
|
_JS_FILE = "case.js"
|
||||||
|
_C_FILE = "case.c"
|
||||||
|
_C_BIN = "case"
|
||||||
|
|
||||||
|
|
||||||
|
def recipe_for(
|
||||||
|
language: str,
|
||||||
|
snippet: str,
|
||||||
|
image: str,
|
||||||
|
sandbox_limits: SandboxConfig,
|
||||||
|
*,
|
||||||
|
repo_root: Path,
|
||||||
|
) -> SandboxJob | None:
|
||||||
|
"""Return a sandbox job for `language`, or None when no recipe exists."""
|
||||||
|
key = language.strip().lower()
|
||||||
|
if key == "python":
|
||||||
|
return _job(
|
||||||
|
image,
|
||||||
|
sandbox_limits,
|
||||||
|
repo_root,
|
||||||
|
command=("env", f"PYTHONPATH={WORKSPACE_MOUNT}", "python", f"{SCRATCH_MOUNT}/{_PYTHON_FILE}"),
|
||||||
|
scratch_files={_PYTHON_FILE: snippet},
|
||||||
|
)
|
||||||
|
if key == "javascript":
|
||||||
|
return _job(
|
||||||
|
image,
|
||||||
|
sandbox_limits,
|
||||||
|
repo_root,
|
||||||
|
command=("env", f"NODE_PATH={WORKSPACE_MOUNT}", "node", f"{SCRATCH_MOUNT}/{_JS_FILE}"),
|
||||||
|
scratch_files={_JS_FILE: snippet},
|
||||||
|
)
|
||||||
|
if key == "c":
|
||||||
|
source = f"{SCRATCH_MOUNT}/{_C_FILE}"
|
||||||
|
binary = f"{SCRATCH_MOUNT}/{_C_BIN}"
|
||||||
|
return _job(
|
||||||
|
image,
|
||||||
|
sandbox_limits,
|
||||||
|
repo_root,
|
||||||
|
command=("sh", "-c", f"cc {source} -o {binary} && {binary}"),
|
||||||
|
scratch_files={_C_FILE: snippet},
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _job(
|
||||||
|
image: str,
|
||||||
|
sandbox_limits: SandboxConfig,
|
||||||
|
repo_root: Path,
|
||||||
|
*,
|
||||||
|
command: tuple[str, ...],
|
||||||
|
scratch_files: dict[str, str],
|
||||||
|
) -> SandboxJob:
|
||||||
|
return SandboxJob(
|
||||||
|
image=image,
|
||||||
|
command=command,
|
||||||
|
repo_root=repo_root,
|
||||||
|
scratch_files=scratch_files,
|
||||||
|
timeout_seconds=sandbox_limits.timeout_seconds,
|
||||||
|
memory_mb=sandbox_limits.memory_mb,
|
||||||
|
pids_limit=sandbox_limits.pids_limit,
|
||||||
|
)
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
"""Language recipes for isolated regression snippets (task 5.2)."""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from openultrasast.config import SandboxConfig
|
||||||
|
from openultrasast.regress import INCONCLUSIVE, RegressionRunner
|
||||||
|
from openultrasast.regress.recipes import recipe_for
|
||||||
|
from openultrasast.sandbox import FakeSandboxRunner, SandboxJob
|
||||||
|
|
||||||
|
LIMITS = SandboxConfig(memory_mb=256, timeout_seconds=30, pids_limit=128)
|
||||||
|
PYTHON_SNIPPET = "assert 1 + 1 == 2\n"
|
||||||
|
JS_SNIPPET = "console.log('ok')\n"
|
||||||
|
C_SNIPPET = "int main(void) { return 0; }\n"
|
||||||
|
JAVA_SNIPPET = "class Case { public static void main(String[] args) {} }\n"
|
||||||
|
|
||||||
|
|
||||||
|
def _repo(tmp_path: Path) -> Path:
|
||||||
|
repo = tmp_path / "repo"
|
||||||
|
repo.mkdir()
|
||||||
|
return repo
|
||||||
|
|
||||||
|
|
||||||
|
def test_python_recipe_runs_scratch_snippet_with_repo_on_pythonpath(tmp_path: Path) -> None:
|
||||||
|
repo = _repo(tmp_path)
|
||||||
|
job = recipe_for("python", PYTHON_SNIPPET, "python:3.12-alpine", LIMITS, repo_root=repo)
|
||||||
|
|
||||||
|
assert isinstance(job, SandboxJob)
|
||||||
|
joined = " ".join(job.command)
|
||||||
|
assert job.image == "python:3.12-alpine"
|
||||||
|
assert job.repo_root == repo
|
||||||
|
assert job.scratch_files == {"case.py": PYTHON_SNIPPET}
|
||||||
|
assert "python" in job.command
|
||||||
|
assert "/scratch/case.py" in job.command
|
||||||
|
assert "PYTHONPATH=/workspace" in joined
|
||||||
|
assert job.timeout_seconds == LIMITS.timeout_seconds
|
||||||
|
assert job.memory_mb == LIMITS.memory_mb
|
||||||
|
assert job.pids_limit == LIMITS.pids_limit
|
||||||
|
|
||||||
|
|
||||||
|
def test_javascript_recipe_runs_scratch_snippet_with_repo_on_node_path(tmp_path: Path) -> None:
|
||||||
|
repo = _repo(tmp_path)
|
||||||
|
job = recipe_for("javascript", JS_SNIPPET, "node:22-alpine", LIMITS, repo_root=repo)
|
||||||
|
|
||||||
|
assert isinstance(job, SandboxJob)
|
||||||
|
joined = " ".join(job.command)
|
||||||
|
assert job.image == "node:22-alpine"
|
||||||
|
assert job.repo_root == repo
|
||||||
|
assert job.scratch_files == {"case.js": JS_SNIPPET}
|
||||||
|
assert "node" in job.command
|
||||||
|
assert "/scratch/case.js" in job.command
|
||||||
|
assert "NODE_PATH=/workspace" in joined
|
||||||
|
assert job.timeout_seconds == LIMITS.timeout_seconds
|
||||||
|
assert job.memory_mb == LIMITS.memory_mb
|
||||||
|
assert job.pids_limit == LIMITS.pids_limit
|
||||||
|
|
||||||
|
|
||||||
|
def test_c_recipe_compiles_and_runs_when_image_has_compiler(tmp_path: Path) -> None:
|
||||||
|
repo = _repo(tmp_path)
|
||||||
|
job = recipe_for("c", C_SNIPPET, "gcc:13", LIMITS, repo_root=repo)
|
||||||
|
|
||||||
|
assert isinstance(job, SandboxJob)
|
||||||
|
joined = " ".join(job.command)
|
||||||
|
assert job.scratch_files == {"case.c": C_SNIPPET}
|
||||||
|
assert "cc" in joined
|
||||||
|
assert "/scratch/case.c" in joined
|
||||||
|
assert "-o" in joined
|
||||||
|
assert "/scratch/case" in joined
|
||||||
|
|
||||||
|
|
||||||
|
def test_unknown_language_recipe_is_none(tmp_path: Path) -> None:
|
||||||
|
repo = _repo(tmp_path)
|
||||||
|
assert recipe_for("java", JAVA_SNIPPET, "eclipse-temurin:21", LIMITS, repo_root=repo) is None
|
||||||
|
assert recipe_for("unknown", "print(1)\n", "python:3.12-alpine", LIMITS, repo_root=repo) is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_python_and_javascript_recipes_produce_runner_jobs(tmp_path: Path) -> None:
|
||||||
|
repo = _repo(tmp_path)
|
||||||
|
python_job = recipe_for("python", PYTHON_SNIPPET, "python:3.12-alpine", LIMITS, repo_root=repo)
|
||||||
|
js_job = recipe_for("javascript", JS_SNIPPET, "node:22-alpine", LIMITS, repo_root=repo)
|
||||||
|
sandbox = FakeSandboxRunner()
|
||||||
|
runner = RegressionRunner(sandbox)
|
||||||
|
|
||||||
|
python_verdict = runner.run_recipe("python", PYTHON_SNIPPET, "python:3.12-alpine", LIMITS, repo_root=repo)
|
||||||
|
js_verdict = runner.run_recipe("javascript", JS_SNIPPET, "node:22-alpine", LIMITS, repo_root=repo)
|
||||||
|
|
||||||
|
assert python_job is not None
|
||||||
|
assert js_job is not None
|
||||||
|
assert python_verdict.reason != "missing_recipe"
|
||||||
|
assert js_verdict.reason != "missing_recipe"
|
||||||
|
assert sandbox.jobs == [python_job, js_job]
|
||||||
|
|
||||||
|
|
||||||
|
def test_unknown_language_returns_inconclusive_without_calling_runner(tmp_path: Path) -> None:
|
||||||
|
repo = _repo(tmp_path)
|
||||||
|
sandbox = FakeSandboxRunner()
|
||||||
|
runner = RegressionRunner(sandbox)
|
||||||
|
|
||||||
|
assert recipe_for("java", JAVA_SNIPPET, "eclipse-temurin:21", LIMITS, repo_root=repo) is None
|
||||||
|
verdict = runner.run_recipe("java", JAVA_SNIPPET, "eclipse-temurin:21", LIMITS, repo_root=repo)
|
||||||
|
|
||||||
|
assert verdict.verdict == INCONCLUSIVE
|
||||||
|
assert verdict.reason == "missing_recipe"
|
||||||
|
assert sandbox.jobs == []
|
||||||
Reference in New Issue
Block a user