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:
MythEclipse committed 2026-09-25 14:04:33 +08:00
1 parent c35b23a37f
commit c6fd9ec268
1317 files changed
+306586 -128

No files matched your search

+45 -9
View File
@@ -90,8 +90,8 @@ async def _proxy(method: str, path: str, body: dict = None):
@app.get("/", response_class=HTMLResponse)
async def index(req: Request):
host = (req.headers.get("host") or "").split(":")[0]
# Team portal domains: <slug>.gemastik.imrnes.team -> their team portal (no admin login)
if host.endswith(".gemastik.imrnes.team") and host != "gemastik.imrnes.team" and host != "panel.gemastik.imrnes.team":
# Team portal domains: <slug>.attackdefense.imrnes.team -> their team portal (no admin login)
if host.endswith(".attackdefense.imrnes.team") and host != "attackdefense.imrnes.team" and host != "panel.attackdefense.imrnes.team":
slug = host.split(".")[0]
for t in orch.list_teams():
if t.get("slug") == slug:
@@ -117,7 +117,7 @@ async def submit_page(req: Request):
return HTMLResponse((BASE_DIR / "static" / "submit.html").read_text())
# ============ Per-team portal (public via <slug>.gemastik.imrnes.team) ============
# ============ Per-team portal (public via <slug>.attackdefense.imrnes.team) ============
@app.get("/team/{idx}", response_class=HTMLResponse)
async def team_portal(idx: int, req: Request):
@@ -194,13 +194,13 @@ def _team_authorized(req: Request, idx: int) -> bool:
def _check_team_host(req: Request, st: dict) -> bool:
"""IDOR guard: host must be this team's own domain (or localhost).
panel.gemastik / gemastik.imrnes.team only allowed with a valid ADMIN session."""
panel.gemastik / attackdefense.imrnes.team only allowed with a valid ADMIN session."""
host = (req.headers.get("host") or "").split(":")[0]
if host == st.get("domain"):
return True
if host.startswith("127.0.0.1") or host.startswith("localhost"):
return True
if host in ("panel.gemastik.imrnes.team", "gemastik.imrnes.team"):
if host in ("panel.attackdefense.imrnes.team", "attackdefense.imrnes.team"):
return _authorized(req) # admin preview only
return False
@@ -234,7 +234,7 @@ async def api_team_targets(idx: int, req: Request):
out.append({
"team_idx": st.get("index"),
"team_label": st.get("label", f"Team {st.get('index')}"),
"domain": st.get("domain") or (st.get("slug", f"team{st.get('index')}") + ".gemastik.imrnes.team"),
"domain": st.get("domain") or (st.get("slug", f"team{st.get('index')}") + ".attackdefense.imrnes.team"),
"port": p["chall"],
})
return {"targets": out}
@@ -357,7 +357,43 @@ def require_login(req: Request):
@app.get("/api/challenges")
async def api_challenges(req: Request):
require_login(req)
return {"challenges": CHALLENGES}
# Full registry (all sets) with enabled status, plus count of live teams
reg = orch.load_registry()
challs = []
for c in reg.get("challenges", []):
challs.append({
"name": c["name"],
"set": c.get("set"),
"category": c.get("category"),
"desc": c.get("desc"),
"enabled": bool(c.get("enabled")),
"chall_offset": c.get("chall_offset"),
"ssh_offset": c.get("ssh_offset"),
"org_port": c.get("org_port"),
"service_dir": c.get("service_dir"),
})
return {"challenges": challs, "hint": "PATCH /api/challenges/<name> with {\"enabled\": bool} to toggle"}
@app.patch("/api/challenges/{challenge}")
async def api_challenge_toggle(challenge: str, req: Request):
require_login(req)
data = await req.json()
enabled = bool(data.get("enabled"))
reg = orch.load_registry()
found = any(c.get("name") == challenge for c in reg.get("challenges", []))
if not found:
raise HTTPException(404, "Unknown challenge")
changed = orch.set_challenge_enabled(challenge, enabled)
# apply to live teams (build/up or stop/remove + receiver restart);
# skip rebuild when the flag didn't actually change
if "unchanged" in changed:
return {"ok": True, "name": challenge, "enabled": enabled, "applied": [], "unchanged": True}
try:
report = await asyncio.to_thread(orch.sync_challenge_runtime, challenge, enabled)
except Exception as e:
raise HTTPException(500, f"Registry updated tapi runtime gagal: {e}")
return {"ok": True, "name": challenge, "enabled": enabled, "applied": report.get("teams", [])}
@app.get("/api/status")
async def api_status(req: Request):
@@ -549,7 +585,7 @@ async def api_topology(req: Request):
require_login(req)
teams = orch.list_teams()
nodes = [
{"id": "panel", "label": "Panel A/D", "type": "panel", "url": "https://panel.gemastik.imrnes.team"},
{"id": "panel", "label": "Panel A/D", "type": "panel", "url": "https://panel.attackdefense.imrnes.team"},
{"id": "traefik", "label": "Traefik / Coolify", "type": "infra"},
{"id": "dns", "label": "*.imrnes.team → 43.134.105.109", "type": "infra"},
]
@@ -687,7 +723,7 @@ async def api_admin_targets(req: Request):
if not (d / "state.json").exists():
continue
st = json.loads((d / "state.json").read_text())
dom = st.get("domain") or (st.get("slug", f"team{st.get('index')}") + ".gemastik.imrnes.team")
dom = st.get("domain") or (st.get("slug", f"team{st.get('index')}") + ".attackdefense.imrnes.team")
for name, coff, soff in orch.CHALLENGES:
p = st["ports"].get(name)
if not p: