From 7398063b798514e27ca15f9ada1aa96c1b4a8484 Mon Sep 17 00:00:00 2001 From: Marius Ciepluch <11855163+norandom@users.noreply.github.com> Date: Fri, 4 Sep 2026 11:50:44 +0200 Subject: [PATCH] feat(three-stage-scan): add aliased-sink mutation the regex scan misses --- .kiro/specs/three-stage-scan/tasks.md | 2 +- benchmarks/fixtures/mutation-python/app.py | 13 ++++++ benchmarks/manifests/mutation-python.toml | 20 ++++++++++ tests/test_benchmark.py | 1 + tests/test_mutation_fixtures.py | 46 ++++++++++++++++++++++ 5 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 benchmarks/fixtures/mutation-python/app.py create mode 100644 benchmarks/manifests/mutation-python.toml create mode 100644 tests/test_mutation_fixtures.py diff --git a/.kiro/specs/three-stage-scan/tasks.md b/.kiro/specs/three-stage-scan/tasks.md index b7d45f5..876df43 100644 --- a/.kiro/specs/three-stage-scan/tasks.md +++ b/.kiro/specs/three-stage-scan/tasks.md @@ -172,7 +172,7 @@ This plan ships the three-stage scan without rebuilding CWE policy or the regex - _Requirements: 8.1_ - _Boundary: Benchmark Harness_ -- [ ] 7.2 (P) Add a mutation of a known sink that a same-line pattern does not match +- [x] 7.2 (P) Add a mutation of a known sink that a same-line pattern does not match - At least one wrapper or intermediate-variable mutation exists per the evaluation plan. - Observable completion: stage 1 misses the mutation; the case is listed as expected in a split-sink or mutation manifest. - _Requirements: 8.2_ diff --git a/benchmarks/fixtures/mutation-python/app.py b/benchmarks/fixtures/mutation-python/app.py new file mode 100644 index 0000000..85cfdad --- /dev/null +++ b/benchmarks/fixtures/mutation-python/app.py @@ -0,0 +1,13 @@ +"""Mutation of os.system: the callable is aliased before use so a same-line regex misses it.""" +import os + +from flask import Flask, request + +app = Flask(__name__) +run_cmd = os.system + + +@app.get("/ping") +def ping(): + host = request.args["host"] + return run_cmd("ping -c 1 " + host) # CWE-78 mutation: os.system aliased before the call diff --git a/benchmarks/manifests/mutation-python.toml b/benchmarks/manifests/mutation-python.toml new file mode 100644 index 0000000..52d97f6 --- /dev/null +++ b/benchmarks/manifests/mutation-python.toml @@ -0,0 +1,20 @@ +name = "mutation-python" +language = "python_web" +frameworks = ["flask"] +setup = ["pip install flask"] +modes = ["quick", "standard"] +known_noise = [] +baselines = [] + +[source] +type = "local" +path = "../fixtures/mutation-python" + +[[expected]] +cwe = "CWE-78" +class = "command injection" +path = "app.py" +line = 13 +function = "ping" +sink = "run_cmd" +evidence = "aliased os.system runs an attacker-controlled command" diff --git a/tests/test_benchmark.py b/tests/test_benchmark.py index 52b500d..8a88f95 100644 --- a/tests/test_benchmark.py +++ b/tests/test_benchmark.py @@ -63,6 +63,7 @@ def test_repository_benchmark_manifests_load_and_point_to_fixtures() -> None: "java-web-smoke.toml", "javascript-node-web-smoke.toml", "javascript-vulnerable.toml", + "mutation-python.toml", "python-vulnerable.toml", "python-web-smoke.toml", "split-sink-c.toml", diff --git a/tests/test_mutation_fixtures.py b/tests/test_mutation_fixtures.py new file mode 100644 index 0000000..f6d38a2 --- /dev/null +++ b/tests/test_mutation_fixtures.py @@ -0,0 +1,46 @@ +"""Mutation fixtures: stage-1 same-line patterns must miss the aliased sink (requirement 8.2).""" + +from pathlib import Path + +from openultrasast.benchmark import BenchmarkRun, evaluate_benchmark, load_benchmark_manifest, resolve_benchmark_source +from openultrasast.findings import StaticFinding, quick_scan_findings +from openultrasast.gate import LANGUAGE_MANIFESTS +from openultrasast.preprocess import preprocess_repository +from openultrasast.rank import rank_targets + +MANIFEST = Path("benchmarks/manifests/mutation-python.toml") + + +def _quick_scan(target: Path) -> list[StaticFinding]: + _, targets = preprocess_repository(target) + return quick_scan_findings(target, targets, rank_targets(targets)) + + +def test_stage1_misses_aliased_os_system_mutation() -> None: + manifest = load_benchmark_manifest(MANIFEST) + target = resolve_benchmark_source(MANIFEST, manifest) + planted = [item for item in manifest.expected if item.rule_id is None] + assert planted, "mutation manifest must document a stage-1 miss without rule_id" + expected = planted[0] + assert expected.sink == "run_cmd" + assert expected.line is not None + + source = (target / expected.path).read_text() + lines = source.splitlines() + assert "os.system" in source + assert "os.system(" not in lines[expected.line - 1] + assert "run_cmd(" in lines[expected.line - 1] + + findings = _quick_scan(target) + on_line = [finding for finding in findings if finding.path == expected.path and finding.line == expected.line] + assert on_line == [], f"stage-1 hit the mutation on {expected.path}:{expected.line}: {on_line}" + + run = BenchmarkRun(benchmark_run_id="mutation", root=Path("/tmp/mutation"), manifest=manifest) + result = evaluate_benchmark(run=run, mode="quick", findings=findings, scan_id=None, scan_run_dir=None) + missed = {(miss.path, miss.cwe, miss.rule_id) for miss in result.misses} + assert (expected.path, expected.cwe, None) in missed + + +def test_mutation_fixture_is_outside_stage1_gate_corpus() -> None: + listed = {name for names in LANGUAGE_MANIFESTS.values() for name in names} + assert "mutation-python" not in listed