feat: challenge registry-driven platform + XVI/XVII imports + admin toggle + domain rename
- Rename repo/domain: attack-defense-platform / attackdefense.imrnes.team (all refs replaced)
- challenge_registry.json: single source of truth (28 challs across gemastik18/xvi/xvii)
- teams.py: registry-driven CHALLENGES, set_challenge_enabled, sync_challenge_runtime
(apply enable/disable to live teams: build/up or stop/remove + receiver restart)
- compose_gen.py: render per-team compose from canonical per-challenge templates
(image reuse, per-team ports 30xxx, flag mounts, passwords)
- gen_canonical_composes.py: canonical docker-compose.yml for all services
- import_new_challenges.py: import XVI/XVII services + EOL base image fixes
(debian:buster→bookworm, node:14→20, python:3.7-slim→3.11)
- receiver: xvi package (10 checkers) + xvii package (12 generic checkers),
Challenge base reads PASSWORD_<team_port> from env; gen_receiver_main.py
generates per-team main.py from registry
- main.py: /api/challenges returns full registry; PATCH /api/challenges/<name>
toggles enabled + applies to live teams
- index.html: 🏗️ Challenge Manager tab (toggle per challenge, grouped by set)
- SLA bonus now dynamic (all enabled challenges, not hardcoded 6)
This commit is contained in:
1 parent
c35b23a37f
commit
c6fd9ec268
1317 files changed
+306586
-128
No files matched your search
@@ -0,0 +1,293 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate canonical docker-compose.yml templates under services/<name>/ for
|
||||
every challenge in the registry (gemastik18 + imported XVI/XVII).
|
||||
|
||||
The generated per-challenge file is the SOURCE for compose_gen — i.e. the
|
||||
per-team compose is rendered from these templates. Uses:
|
||||
|
||||
services/<name>/docker-compose.yml (canonical, uniform)
|
||||
|
||||
If a template already exists for gemastik18 challenges (from the shared
|
||||
compose), keep it. For imported challenges, build one from the registry.
|
||||
"""
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
BASE = Path("/opt/gemastik18-final")
|
||||
SVC = BASE / "services"
|
||||
TEAMS_DIR = BASE / "teams"
|
||||
|
||||
def load_registry():
|
||||
return json.loads((TEAMS_DIR / "challenge_registry.json").read_text())
|
||||
|
||||
def internal_port_for(ch: dict) -> int:
|
||||
"""Best-effort: the container's internal listening port."""
|
||||
name = ch["name"]
|
||||
known = {
|
||||
# web-ish default 8000, others from upstream compose
|
||||
"art": 8080, "xl": 3000, "gemas-notes": 80, "pasta": 8000,
|
||||
"burvesigner": 80, "hirnfick": 8000, "gemas-fetcher": 8000,
|
||||
"s3": 80, "crawlback": 80, "back-to-basic": 8000,
|
||||
"anti-alchemy": 5000, "asmr": 8000, "bit-canvas": 8000,
|
||||
"fjb": 80, "gift-card": 5000, "gift-voucher": 5000,
|
||||
"gleam-drive": 8000, "go-green": 8000, "kode-viewer": 3000,
|
||||
"more-less": 8000, "tempest-poc": 80, "ticketer": 5000,
|
||||
}
|
||||
return known.get(name, 8000)
|
||||
|
||||
def sidecar_for(ch: dict) -> dict:
|
||||
"""Return {sidecar_name: {image, env, volumes, cmd...}} or {}."""
|
||||
name = ch["name"]
|
||||
if name == "gemas-notes":
|
||||
return {
|
||||
"database": {
|
||||
"image": "mysql:8",
|
||||
"container_name": None, # per-team
|
||||
"environment": ["MYSQL_ROOT_PASSWORD=why_my_random_string_password_doesnot_working",
|
||||
"MYSQL_DATABASE=gemasnotes"],
|
||||
"volumes": ["./gemas-notes/src/db/init.sql:/docker-entrypoint-initdb.d/init.sql"],
|
||||
},
|
||||
"validation-service": {
|
||||
"build": "./gemas-notes/src/validation-service",
|
||||
"container_name": None,
|
||||
"depends_on": ["database"],
|
||||
"ports_chall": 80, # challenge port exposed here (REST API)
|
||||
},
|
||||
}
|
||||
if name == "gemas-fetcher":
|
||||
return {
|
||||
"mongodb": {
|
||||
"image": "mongo:4.4",
|
||||
"container_name": None,
|
||||
"environment": ["MONGO_INITDB_ROOT_USERNAME=ctf",
|
||||
"MONGO_INITDB_ROOT_PASSWORD=asjdkjk23j1k3dsdn2h233j3jj",
|
||||
"MONGO_INITDB_DATABASE=web_fetcher"],
|
||||
}
|
||||
}
|
||||
if name == "kode-viewer":
|
||||
return {
|
||||
"redis": {
|
||||
"image": "redis:alpine",
|
||||
"container_name": None,
|
||||
"volumes": ["./redis-data:/data"],
|
||||
}
|
||||
}
|
||||
if name == "anti-alchemy":
|
||||
return {
|
||||
"anti-alchemy-db": {
|
||||
"image": "postgres:16.3-alpine",
|
||||
"container_name": None,
|
||||
"environment": ["POSTGRES_USER=postgres", "POSTGRES_PASSWORD=password"],
|
||||
"volumes": ["./db/dump.sql:/docker-entrypoint-initdb.d/init.sql"],
|
||||
}
|
||||
}
|
||||
if name == "tempest-poc":
|
||||
return {
|
||||
"backend": {
|
||||
"build": "./tempest-poc/backend",
|
||||
"container_name": None,
|
||||
"ports_ssh": 22,
|
||||
"extra_hosts": True,
|
||||
},
|
||||
"frontend": {
|
||||
"build": "./tempest-poc/frontend",
|
||||
"container_name": None,
|
||||
"ports_chall": 80,
|
||||
"extra_hosts": True,
|
||||
}
|
||||
}
|
||||
return {}
|
||||
|
||||
def render(ch: dict) -> str:
|
||||
name = ch["name"]
|
||||
org = int(ch.get("org_port", 10000))
|
||||
internal = internal_port_for(ch)
|
||||
sidecars = sidecar_for(ch)
|
||||
# For tempest-poc the main (first) service is frontend; compose_gen's
|
||||
# SIDECAR_NAMES marks backend as sidecar. Order matters: put frontend
|
||||
# before backend in the generated file so compose_gen picks frontend as main.
|
||||
if name == "tempest-poc":
|
||||
sidecars = {"frontend": sidecars.pop("frontend"), **sidecars}
|
||||
lines = ["services:"]
|
||||
# main service
|
||||
cont = f"{name}_container"
|
||||
lines += [
|
||||
f" {name}:",
|
||||
f" container_name: {cont}",
|
||||
f" hostname: {name}",
|
||||
" restart: always",
|
||||
" build:",
|
||||
" context: .",
|
||||
" args:",
|
||||
f" - PASSWORD=$PASSWORD_{org}",
|
||||
]
|
||||
# volumes: flag + bashrc + preexec (uniform unless challenge differs)
|
||||
flag_path = f"../receiver/flags/{name}.txt:/flag.txt:ro"
|
||||
# gift-card/gift-voucher mount to /ctf/<name>/flag.txt
|
||||
if name in ("gift-card", "gift-voucher"):
|
||||
flag_path = f"../receiver/flags/{name}.txt:/ctf/{name}/flag.txt:ro"
|
||||
if name == "pasta":
|
||||
flag_path = f"../receiver/flags/{name}.txt:/ctf/pasta/flag.txt:ro"
|
||||
lines += [
|
||||
" volumes:",
|
||||
f" - {flag_path}",
|
||||
" - ../utils/bashrc:/root/.bashrc:ro",
|
||||
" - ../utils/preexec.sh:/root/.preexec.sh:ro",
|
||||
]
|
||||
# ports: external (org / org+22) but rewritten per team by composer
|
||||
if name == "gemas-notes":
|
||||
# main service = note-service (Go, ssh only). The challenge port is
|
||||
# exposed by validation-service (the REST API the checker hits).
|
||||
pass
|
||||
elif name == "tempest-poc":
|
||||
# main (frontend) exposes chall port; backend (sidecar) has ssh.
|
||||
pass
|
||||
else:
|
||||
lines += [
|
||||
" ports:",
|
||||
f" - \"{org}:{internal}\"",
|
||||
f" - \"{org + 22}:22\"",
|
||||
]
|
||||
lines.append(" extra_hosts:")
|
||||
lines.append(' - "host.docker.internal:host-gateway"')
|
||||
# env (challenge-specific)
|
||||
if name == "carbeat":
|
||||
lines.append(" environment:")
|
||||
lines.append(" - FLAG=GEMASTIK18{local_flag}")
|
||||
if name == "phew":
|
||||
lines.append(" environment:")
|
||||
lines.append(" - FLAG=GEMASTIK18{local_flag}")
|
||||
if name == "sheesh":
|
||||
lines.append(" environment:")
|
||||
lines.append(" - FLAG=GEMASTIK18{local_flag}")
|
||||
if name == "anti-alchemy":
|
||||
lines.append(" environment:")
|
||||
lines.append(" - DB_NAME=postgres")
|
||||
lines.append(" - DB_USER=postgres")
|
||||
lines.append(" - DB_PASS=password")
|
||||
lines.append(" - DB_HOST=anti-alchemy-db")
|
||||
lines.append(" - DB_PORT=5432")
|
||||
lines.append(f" - SECRET_KEY=$PASSWORD_{org}")
|
||||
lines.append(" depends_on:")
|
||||
lines.append(" - anti-alchemy-db")
|
||||
if name == "gemas-fetcher":
|
||||
lines.append(" environment:")
|
||||
lines.append(" - MONGO_URI=mongodb://ctf:asjdkjk23j1k3dsdn2h233j3jj@mongodb:27017/web_fetcher?authSource=admin")
|
||||
lines.append(f" - APP_URI=http://0.0.0.0:{org}")
|
||||
lines.append(" depends_on:")
|
||||
lines.append(" - mongodb")
|
||||
if name == "kode-viewer":
|
||||
lines.append(" environment:")
|
||||
lines.append(" - REDIS_HOST=redis")
|
||||
lines.append(" - REDIS_PORT=6379")
|
||||
lines.append(" depends_on:")
|
||||
lines.append(" - redis")
|
||||
if name == "gemas-notes":
|
||||
lines.append(" ports:")
|
||||
lines.append(f" - \"{org + 22}:22\"")
|
||||
lines.append(" depends_on:")
|
||||
lines.append(" - database")
|
||||
lines.append(" - validation-service")
|
||||
# note: gemas-notes internal port differs (validation-service is the 8000 listener?)
|
||||
# upstream maps validation-service:12000:80 and gemas-notes has no port except ssh.
|
||||
# Our single main service is gemas-notes which exposes ssh on 22.
|
||||
if name == "burvesigner":
|
||||
lines.append(" volumes:")
|
||||
lines.append(" - ../receiver/files/burvesigner.priv:/priv.data:ro")
|
||||
# sidecars
|
||||
for sname, sc in sidecars.items():
|
||||
cont_s = f"{sname}_container"
|
||||
lines.append(f" {sname}:")
|
||||
if sc.get("build"):
|
||||
lines += [" build:",
|
||||
f" context: {sc['build']}",
|
||||
f" dockerfile: Dockerfile"]
|
||||
elif sc.get("image"):
|
||||
lines.append(f" image: {sc['image']}")
|
||||
if sc.get("container_name") is not None:
|
||||
lines.append(f" container_name: {cont_s}")
|
||||
if sc.get("environment"):
|
||||
lines.append(" environment:")
|
||||
for e in sc["environment"]:
|
||||
lines.append(f" - {e}")
|
||||
if sc.get("volumes"):
|
||||
lines.append(" volumes:")
|
||||
for v in sc["volumes"]:
|
||||
lines.append(f" - {v}")
|
||||
if sc.get("depends_on"):
|
||||
lines.append(" depends_on:")
|
||||
for dep in sc["depends_on"]:
|
||||
lines.append(f" - {dep}")
|
||||
if sc.get("ports_chall"):
|
||||
lines.append(" ports:")
|
||||
lines.append(f" - \"{org}:{sc['ports_chall']}\"")
|
||||
if sc.get("ports_ssh"):
|
||||
lines.append(" ports:")
|
||||
lines.append(f" - \"{org + 22}:{sc['ports_ssh']}\"")
|
||||
if sc.get("extra_hosts"):
|
||||
lines.append(" extra_hosts:")
|
||||
lines.append(' - "host.docker.internal:host-gateway"')
|
||||
if sname == "frontend" and name == "tempest-poc":
|
||||
# tempest frontend already handled via ports_chall
|
||||
pass
|
||||
elif sname == "frontend":
|
||||
lines += [" ports:", f" - \"{org}:80\""]
|
||||
lines.append(" extra_hosts:")
|
||||
lines.append(' - "host.docker.internal:host-gateway"')
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
def main():
|
||||
reg = load_registry()
|
||||
for ch in reg["challenges"]:
|
||||
name = ch["name"]
|
||||
dst = SVC / name / "docker-compose.yml"
|
||||
if name in ("blogpost", "carbeat", "cdn", "phew", "sheesh", "warmup"):
|
||||
# keep existing split template (from shared compose)
|
||||
tpl = SVC / name / "compose.template.yml"
|
||||
if tpl.exists() and not dst.exists():
|
||||
text = tpl.read_text()
|
||||
lines = [l for l in text.splitlines() if l.strip() and not l.strip().startswith("#")]
|
||||
out = []
|
||||
for l in lines:
|
||||
if l.strip().startswith("# ---") and out:
|
||||
break
|
||||
out.append(l)
|
||||
dst.write_text("\n".join(out).rstrip() + "\n")
|
||||
print(f"kept template: {name}")
|
||||
continue
|
||||
if name == "tempest-poc":
|
||||
dst.write_text(render_tempest(ch))
|
||||
print(f"wrote canonical compose (tempest): {name}")
|
||||
continue
|
||||
dst.write_text(render(ch))
|
||||
print(f"wrote canonical compose: {name}")
|
||||
|
||||
|
||||
def render_tempest(ch: dict) -> str:
|
||||
"""tempest-poc: frontend (nginx) is the MAIN challenge service — it gets
|
||||
the challenge container name + SSH + challenge port; backend is a sidecar
|
||||
service that frontend proxies to (no host port needed)."""
|
||||
name = ch["name"]
|
||||
org = int(ch.get("org_port", 10000))
|
||||
return f"""services:
|
||||
{name}:
|
||||
container_name: {name}_container
|
||||
hostname: {name}
|
||||
restart: always
|
||||
build:
|
||||
context: ./tempest-poc/frontend
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- "{org}:80"
|
||||
- "{org + 22}:22"
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
backend:
|
||||
build:
|
||||
context: ./tempest-poc/backend
|
||||
dockerfile: Dockerfile
|
||||
"""
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in new issue
Block a user