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
+45
-9
@@ -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:
|
||||
|
||||
Reference in new issue
Block a user