mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-16 11:49:28 +00:00
146 lines
7.5 KiB
Python
146 lines
7.5 KiB
Python
"""learning-harness task 2.7: canary templates and the second judge (offline)."""
|
|||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import json
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
from openultrasast.config import SandboxConfig
|
||
|
|
from openultrasast.findings import StaticFinding
|
||
|
|
from openultrasast.learning.families import load_families
|
||
|
|
from openultrasast.regress.safety import check_snippet_safety
|
||
|
|
from openultrasast.sandbox import SandboxResult
|
||
|
|
from openultrasast.sandbox.runner import FakeSandboxRunner
|
||
|
|
from openultrasast.tool_hunter import ChatResponse, ScriptedChatClient
|
||
|
|
|
||
|
|
CANARY_FAMILIES = ("injection", "path", "deserialization")
|
||
|
|
|
||
|
|
|
||
|
|
def _claim(family: str, function: str = "lookup", line: int = 3) -> StaticFinding:
|
||
|
|
return StaticFinding(
|
||
|
|
finding_id=f"detector:app.py:{line}",
|
||
|
|
path="app.py",
|
||
|
|
title="t",
|
||
|
|
severity="high",
|
||
|
|
confidence="low",
|
||
|
|
evidence_level="suspicion",
|
||
|
|
rationale="r",
|
||
|
|
line=line,
|
||
|
|
function_name=function,
|
||
|
|
reachability_status="unknown",
|
||
|
|
reachability_evidence=[],
|
||
|
|
reachability_conditions=[],
|
||
|
|
tags=[f"family:{family}"],
|
||
|
|
ranking_priority=0.0,
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def test_every_canary_template_renders_and_passes_the_snippet_safety_check() -> None:
|
||
|
|
from openultrasast.learning.canaries import render_canary
|
||
|
|
|
||
|
|
for family in CANARY_FAMILIES:
|
||
|
|
snippet, token = render_canary(family, language="python", path="app.py", function="lookup")
|
||
|
|
assert token and token in snippet
|
||
|
|
check_snippet_safety(snippet) # raises when the template would be refused before it ever runs
|
||
|
|
assert "/workspace" in snippet # the candidate is imported from the read-only source mount
|
||
|
|
assert render_canary("memory", language="python", path="app.py", function="f") is None
|
||
|
|
assert render_canary("injection", language="rust", path="a.rs", function="f") is None
|
||
|
|
|
||
|
|
|
||
|
|
def test_a_canary_token_is_unique_per_claim_so_one_family_never_answers_for_another() -> None:
|
||
|
|
from openultrasast.learning.canaries import render_canary
|
||
|
|
|
||
|
|
first = render_canary("injection", language="python", path="app.py", function="lookup")
|
||
|
|
second = render_canary("injection", language="python", path="app.py", function="other")
|
||
|
|
third = render_canary("path", language="python", path="app.py", function="lookup")
|
||
|
|
assert first and second and third
|
||
|
|
assert first[1] != second[1] != third[1] and first[1] != third[1]
|
||
|
|
|
||
|
|
|
||
|
|
def test_the_canary_verifier_proves_only_when_the_oracle_fires(tmp_path: Path) -> None:
|
||
|
|
from openultrasast.learning.canaries import CanaryVerifier
|
||
|
|
|
||
|
|
(tmp_path / "app.py").write_text("def lookup(value):\n return value\n")
|
||
|
|
fired = CanaryVerifier(runner=_runner(lambda token: SandboxResult(0, f"CANARY {token}", "", False)), limits=SandboxConfig())
|
||
|
|
proven = fired.verify(_claim("injection"), root=tmp_path, language="python")
|
||
|
|
assert proven.rung == "proven" and proven.reason == "oracle_fired"
|
||
|
|
quiet = CanaryVerifier(runner=_runner(lambda _t: SandboxResult(1, "boom", "Traceback", False)), limits=SandboxConfig())
|
||
|
|
unproven = quiet.verify(_claim("injection"), root=tmp_path, language="python")
|
||
|
|
assert unproven.rung == "suspicion" and unproven.reason == "oracle_quiet" # a crash is not this family's evidence
|
||
|
|
|
||
|
|
|
||
|
|
def test_a_family_or_language_with_no_template_stays_at_suspicion(tmp_path: Path) -> None:
|
||
|
|
from openultrasast.learning.canaries import CanaryVerifier
|
||
|
|
|
||
|
|
(tmp_path / "app.py").write_text("def lookup(value):\n return value\n")
|
||
|
|
verifier = CanaryVerifier(runner=_runner(lambda token: SandboxResult(0, token, "", False)), limits=SandboxConfig())
|
||
|
|
assert verifier.verify(_claim("memory"), root=tmp_path, language="python").reason == "no_template"
|
||
|
|
assert verifier.verify(_claim("injection"), root=tmp_path, language="rust").reason == "no_template"
|
||
|
|
|
||
|
|
|
||
|
|
def test_the_registry_uses_the_canary_verifier_when_one_is_supplied(tmp_path: Path) -> None:
|
||
|
|
from openultrasast.learning.canaries import CanaryVerifier
|
||
|
|
from openultrasast.learning.verifiers import NoVerifier, verifier_for
|
||
|
|
|
||
|
|
taxonomy = load_families()
|
||
|
|
canary = CanaryVerifier(runner=_runner(lambda token: SandboxResult(0, token, "", False)), limits=SandboxConfig())
|
||
|
|
assert verifier_for(taxonomy.by_id("injection"), canary=canary) is canary
|
||
|
|
assert isinstance(verifier_for(taxonomy.by_id("access_control"), canary=canary), type(verifier_for(taxonomy.by_id("access_control"))))
|
||
|
|
assert isinstance(verifier_for(taxonomy.by_id("memory"), canary=canary), NoVerifier)
|
||
|
|
|
||
|
|
|
||
|
|
def test_nothing_is_published_as_proven_until_a_second_judge_confirms() -> None:
|
||
|
|
from openultrasast.learning.canaries import confirm_proven
|
||
|
|
from openultrasast.learning.verifiers import Verification
|
||
|
|
|
||
|
|
proven = Verification(rung="proven", reason="oracle_fired", oracle_output="CANARY abc")
|
||
|
|
agrees = ScriptedChatClient([ChatResponse(content=json.dumps({"confirms": True, "reason": "the row came back"}))])
|
||
|
|
kept = confirm_proven(proven, _claim("injection"), client=agrees, model="judge", snippet="print('x')")
|
||
|
|
assert kept.rung == "proven"
|
||
|
|
declines = ScriptedChatClient([ChatResponse(content=json.dumps({"confirms": False, "reason": "unrelated output"}))])
|
||
|
|
dropped = confirm_proven(proven, _claim("injection"), client=declines, model="judge", snippet="print('x')")
|
||
|
|
assert dropped.rung == "static_corroboration" and dropped.reason == "judge_declined"
|
||
|
|
assert confirm_proven(proven, _claim("injection"), client=None, model="", snippet="x").rung == "static_corroboration"
|
||
|
|
lower = Verification(rung="suspicion", reason="oracle_quiet")
|
||
|
|
assert confirm_proven(lower, _claim("injection"), client=agrees, model="judge", snippet="x") is lower # nothing to confirm
|
||
|
|
|
||
|
|
|
||
|
|
def test_the_judge_sees_the_snippet_and_the_oracle_output_and_never_the_label() -> None:
|
||
|
|
from openultrasast.learning.canaries import confirm_proven
|
||
|
|
from openultrasast.learning.verifiers import Verification
|
||
|
|
|
||
|
|
client = ScriptedChatClient([ChatResponse(content=json.dumps({"confirms": True, "reason": "ok"}))])
|
||
|
|
confirm_proven(
|
||
|
|
Verification(rung="proven", reason="oracle_fired", oracle_output="CANARY abc"),
|
||
|
|
_claim("injection"),
|
||
|
|
client=client,
|
||
|
|
model="judge",
|
||
|
|
snippet="print('CANARY abc')",
|
||
|
|
)
|
||
|
|
prompt = json.dumps(client.calls[0]["messages"])
|
||
|
|
assert "CANARY abc" in prompt and "print('CANARY abc')" in prompt
|
||
|
|
assert client.calls[0].get("json_object") is True and client.calls[0]["tools"] == []
|
||
|
|
assert "family:injection" not in prompt # the judge is told the claim, not the answer key
|
||
|
|
|
||
|
|
|
||
|
|
def test_templates_and_oracle_code_live_outside_every_proposer_write_root(tmp_path: Path) -> None:
|
||
|
|
from openultrasast.learning import canaries
|
||
|
|
from openultrasast.learning.detectors import write_default_configs
|
||
|
|
|
||
|
|
write_roots = set(write_default_configs(tmp_path, load_families(), prompt="p", version="0").values())
|
||
|
|
module = Path(canaries.__file__).resolve()
|
||
|
|
assert not any(module.is_relative_to(root.resolve()) for root in write_roots)
|
||
|
|
assert "site-packages" in str(module) or "src/openultrasast" in str(module)
|
||
|
|
|
||
|
|
|
||
|
|
def _runner(reply: object) -> FakeSandboxRunner:
|
||
|
|
"""A sandbox that answers from whatever canary token the snippet actually planted."""
|
||
|
|
|
||
|
|
class _Runner(FakeSandboxRunner): # type: ignore[misc]
|
||
|
|
def run(self, spec: object) -> SandboxResult:
|
||
|
|
body = "\n".join(str(value) for value in (getattr(spec, "scratch_files", {}) or {}).values())
|
||
|
|
marker = next((word.strip("'\"") for word in body.split() if "ousast-canary-" in word), "ousast-canary-none")
|
||
|
|
return reply(marker) # type: ignore[operator]
|
||
|
|
|
||
|
|
return _Runner()
|