fix(bench): discover the challenge port dynamically and tear down on deploy failure

The suite does not use one container port: most challenges publish 8080
but some (051, 091, 100) publish 5000, and the adapter had 8080
hardcoded, so deploy failed on those and the failure happened before
the teardown block, leaving the challenge running.

- the host port now comes from the app container port mapping (any
  container port, via docker inspect), never from an assumed 8080
- the teardown finally now wraps the deploy as well: a failure halfway
  through deploy no longer leaves containers behind (--keep and
  --deploy-only still opt out)
This commit is contained in:
BaaRai
2026-10-16 14:29:00 +02:00
parent 623e5e58a7
commit 89ece8eb8d
2 changed files with 43 additions and 18 deletions
+22 -4
View File
@@ -36,7 +36,6 @@ FLAG_RE = re.compile(r"(?:FLAG\{[0-9a-f]{64}\}|flag\{[0-9a-f-]{36}\})")
CHALLENGE_RE = re.compile(r"DBEN-v1-\d{3}")
APP_SERVICE = "app"
APP_CONTAINER_PORT = "8080"
def random_flag() -> str:
@@ -80,13 +79,33 @@ def compose_project(challenge: str, run_id: str) -> str:
def host_port(output: str) -> int:
"""Parse the host port out of `docker compose port app 8080` output."""
"""Parse the host port out of `docker compose port` output."""
line = output.strip().splitlines()[-1] if output.strip() else ""
if ":" not in line:
raise SystemExit(f"could not parse a host port from docker compose output: {output!r}")
return int(line.rsplit(":", 1)[1])
def container_host_port(project: str, cdir: pathlib.Path) -> int:
"""Host port published by the app service, whatever container port the
challenge declares (most challenges use 8080, some use 5000: never assume)."""
res = _compose(project, cdir, ["ps", "-q", APP_SERVICE], timeout=60)
cid = res.stdout.strip().splitlines()[0] if res.stdout.strip() else ""
if not cid:
raise SystemExit(f"no running {APP_SERVICE} container for project {project}")
insp = subprocess.run(
["docker", "inspect", "--format",
"{{range $p, $conf := .NetworkSettings.Ports}}{{(index $conf 0).HostPort}}\n{{end}}",
cid],
capture_output=True, text=True, timeout=60)
for token in insp.stdout.split():
if token.isdigit():
return int(token)
raise SystemExit(
f"no published host port on container {cid} "
f"({insp.stderr.strip()[:200] or 'inspect returned no port mapping'})")
def derive_ground_truth(repo: str, challenge: str) -> dict:
"""Ground truth from the challenge's own benchmark.json.
@@ -168,8 +187,7 @@ def deploy(repo: str, challenge: str, flag: str, run_id: str) -> dict:
if up.returncode != 0:
print("[drost] up --wait failed; falling back to up -d and polling")
_compose(project, cdir, ["up", "-d"], timeout=900)
res = _compose(project, cdir, ["port", APP_SERVICE, APP_CONTAINER_PORT], timeout=60)
url = f"http://127.0.0.1:{host_port(res.stdout)}"
url = f"http://127.0.0.1:{container_host_port(project, cdir)}"
for _ in range(60):
try:
urllib.request.urlopen(url, timeout=5)
+21 -14
View File
@@ -472,17 +472,24 @@ def main() -> None:
gt = json.loads(gt_path.read_text())
gt_findings = gt["findings"]
dep = deploy_corpus(args)
corpus_url = dep["url"]
flag = dep.get("flag")
if args.deploy_only:
print(f"[drost] deploy-only: up at {corpus_url}, flag {flag}")
print(f"[drost] stop it later with: docker compose -p {dep['project']} down --remove-orphans"
f" (cwd {dep['challenge_dir']})")
return
# drost project/dir are deterministic, so they are known before deploy and
# the finally below can clean up even when the deploy itself fails halfway.
drost_project = drost_cdir = None
if args.corpus_type == "drost":
drost_project = drost.compose_project(args.drost_id, args.run_id)
drost_cdir = str(drost.challenge_dir(args.drost_repo, args.drost_id))
task_url = corpus_url
try:
dep = deploy_corpus(args)
corpus_url = dep["url"]
flag = dep.get("flag")
if args.deploy_only:
print(f"[drost] deploy-only: up at {corpus_url}, flag {flag}")
print(f"[drost] stop it later with: docker compose -p {dep['project']} down --remove-orphans"
f" (cwd {dep['challenge_dir']})")
return
task_url = corpus_url
if args.target_host != "127.0.0.1":
task_url = f"http://{args.target_host}:{corpus_url.rsplit(':', 1)[1]}"
print(f"[corpus] task target rewritten for ARTEX reachability: {task_url}")
@@ -534,12 +541,12 @@ def main() -> None:
write_report(out_dir, args.corpus, args, task_info, status, findings, verdicts, result)
print(json.dumps(result, indent=2)[:1500])
finally:
# The corpus is torn down even when scoring fails halfway: a crash in
# the report phase must not leave the challenge running (--keep opts
# out deliberately).
# The corpus is torn down even when a step fails halfway: a crash in
# deploy, scoring or reporting must not leave the challenge running
# (--keep and --deploy-only opt out deliberately).
if args.corpus_type == "drost":
if not args.keep:
drost.teardown(dep["project"], dep["challenge_dir"])
if not (args.keep or args.deploy_only):
drost.teardown(drost_project, drost_cdir)
elif not (args.keep or args.corpus_url):
subprocess.run(["docker", "rm", "-f", f"bench-{args.corpus}-{args.run_id}"], capture_output=True)