Files
ARTEX/scripts/benchmark/test_drost.py
T
BaaRai d2f4697b2b feat(bench): add the DrostBench corpus with a flag oracle
DrostBench v1 ships 100 docker-compose web challenges, each with one
flag-bearing vulnerability plus decoys that do not reach the flag, so
flag capture is an objective win condition that no judge decides.

scripts/benchmark/drost.py drives one challenge: compose build with an
explicit per-build random flag (the suite's deterministic sha256 default
is never used, so a memorized flag cannot be replayed), up --wait, host
port discovery, down --rmi local. Ground truth is derived from the
challenge benchmark.json; the flag is scanned over the fetched findings
first and over full details, facts and the report only before declaring
a miss.

run_bench.py gains --corpus-type drost (one challenge per invocation),
flag capture in every drost report, a flag-only mode when no judge is
configured (gates reported as not measured, never as zero), and
--deploy-only for install validation. Image mode is unchanged.

test_drost.py covers the pure functions (flag formats, challenge id
validation, port parsing, ground-truth derivation incl. its refusals);
the lifecycle was exercised end to end on DBEN-v1-001: build with a
random flag, reachable app, compiled flag matching the override,
teardown without leftovers.

Closes #94
2026-10-16 11:27:54 +02:00

111 lines
4.4 KiB
Python
Executable File

#!/usr/bin/env python3
"""Unit tests for the DrostBench corpus adapter (pure functions only; the
docker lifecycle is exercised by a real deploy, not here).
Run: python3 -m unittest test_drost -v
"""
from __future__ import annotations
import json
import pathlib
import tempfile
import unittest
import drost
class FlagTests(unittest.TestCase):
def test_random_flag_shape(self):
self.assertRegex(drost.random_flag(), r"^flag\{[0-9a-f-]{36}\}$")
def test_validate_flag_accepts_both_suite_formats(self):
drost.validate_flag("flag{2446f870-cd6d-4106-9864-4f227ce177d3}")
drost.validate_flag("FLAG{" + "ab" * 32 + "}")
def test_validate_flag_rejects_non_suite_formats(self):
for bad in ("flag{short}", "FLAG{" + "zz" * 32 + "}", "nope", "flag{2446F870-cd6d-4106-9864-4f227ce177d3}"):
with self.assertRaises(SystemExit):
drost.validate_flag(bad)
def test_flag_in_texts(self):
f = drost.random_flag()
self.assertTrue(drost.flag_in_texts(f, ["nothing", f"loot: {f}\n"]))
self.assertFalse(drost.flag_in_texts(f, ["nothing here"]))
class ChallengeTests(unittest.TestCase):
def test_challenge_validation(self):
drost.validate_challenge("DBEN-v1-001")
for bad in ("DBEN-001", "DBEN-v1-1", "DBEN-v1-0001", "../DBEN-v1-001", "DBEN-v1-001/../../etc"):
with self.assertRaises(SystemExit):
drost.validate_challenge(bad)
def test_compose_project_sanitized(self):
self.assertEqual(drost.compose_project("DBEN-v1-001", "20261016-120000"), "dben-v1-001-20261016-120000")
self.assertEqual(drost.compose_project("DBEN-v1-001", "run:1!"), "dben-v1-001-run1")
def test_host_port_parsing(self):
self.assertEqual(drost.host_port("0.0.0.0:32768\n"), 32768)
self.assertEqual(drost.host_port("[::]:49153"), 49153)
with self.assertRaises(SystemExit):
drost.host_port("no port here")
class GroundTruthTests(unittest.TestCase):
def _fixture(self, tmp: str, name: str, description: str = "Sign petitions as another user.", tags=None) -> str:
repo = pathlib.Path(tmp)
cdir = repo / "benchmarks" / "DBEN-v1-001"
cdir.mkdir(parents=True)
(cdir / "benchmark.json").write_text(json.dumps({
"name": name,
"description": description,
"level": 1,
"win_condition": "flag",
"tags": tags if tags is not None else ["csrf"],
}))
return str(repo)
def test_derive_from_benchmark_json(self):
with tempfile.TemporaryDirectory() as tmp:
repo = self._fixture(tmp, "DBEN-v1-001 Petitions")
gt = drost.derive_ground_truth(repo, "DBEN-v1-001")
self.assertEqual(gt["corpus"], "drost-benchmarks/DBEN-v1-001")
self.assertTrue(gt["version"].startswith("DBEN-v1-001@"))
self.assertIn("Sign petitions", gt["task_goal_template"])
self.assertIn("flag", gt["task_goal_template"].lower())
entry = gt["findings"][0]
self.assertEqual(entry["id"], "GT-FLAG")
self.assertEqual(entry["classes"], ["csrf"])
self.assertEqual(entry["components"], [])
self.assertEqual(entry["proof_hint"], "Sign petitions as another user.")
def test_derive_refuses_name_not_prefixed_by_id(self):
with tempfile.TemporaryDirectory() as tmp:
repo = self._fixture(tmp, "Petitions")
with self.assertRaises(SystemExit):
drost.derive_ground_truth(repo, "DBEN-v1-001")
def test_derive_refuses_missing_description(self):
with tempfile.TemporaryDirectory() as tmp:
repo = self._fixture(tmp, "DBEN-v1-001 Petitions", description="")
with self.assertRaises(SystemExit):
drost.derive_ground_truth(repo, "DBEN-v1-001")
def test_derive_refuses_missing_challenge(self):
with tempfile.TemporaryDirectory() as tmp:
with self.assertRaises(SystemExit):
drost.derive_ground_truth(tmp, "DBEN-v1-042")
class FindingTextTests(unittest.TestCase):
def test_finding_texts_collects_the_three_fields(self):
findings = [{"name": "n", "summary": "s", "report": "r", "id": 7}]
self.assertEqual(drost.finding_texts(findings), ["n", "s", "r"])
def test_finding_texts_tolerates_missing_fields(self):
self.assertEqual(drost.finding_texts([{"id": 7}]), ["", "", ""])
if __name__ == "__main__":
unittest.main()