diff --git a/panel/main.py b/panel/main.py index c043fea..d47d634 100644 --- a/panel/main.py +++ b/panel/main.py @@ -202,7 +202,7 @@ async def api_team_session(idx: int, req: Request): @app.get("/api/team/{idx}/targets") async def api_team_targets(idx: int, req: Request): - """Team targets — requires team login + own host.""" + """Team targets — requires team login + own host. Only domain + port.""" td = orch.TEAMS_DIR / f"team{idx}" if not (td / "state.json").exists(): raise HTTPException(404, "Team not found") @@ -223,12 +223,8 @@ async def api_team_targets(idx: int, req: Request): if not p: continue out.append({ - "team": st.get("label", f"Team {st.get('index')}"), - "team_idx": st.get("index"), - "domain": st.get("domain"), - "challenge": name, + "domain": st.get("domain") or (st.get("slug", f"team{st.get('index')}") + ".gemastik.imrnes.team"), "port": p["chall"], - "ssh": p["ssh"], }) return {"targets": out} @@ -510,6 +506,19 @@ async def api_randomize(idx: int, req: Request): raise HTTPException(500, str(e)) +@app.post("/api/reset/scores") +async def api_reset_scores(req: Request): + """Admin: wipe leaderboard (all solves).""" + require_login(req) + return orch.reset_scores() + +@app.post("/api/reset/environment") +async def api_reset_environment(req: Request): + """Admin: full environment reset (stop all, remove teams/containers/receivers).""" + require_login(req) + return orch.reset_environment() + + @app.post("/api/flag/submit") async def api_flag_submit(req: Request): """Public endpoint: teams submit flags. No login needed.""" @@ -549,6 +558,29 @@ async def api_public_teams(): """Public list of team names (for the submit dropdown).""" return {"teams": [{"index": t["index"], "label": t.get("label", f"Team {t['index']}")} for t in orch.list_teams()]} +@app.get("/api/targets") +async def api_admin_targets(req: Request): + """Admin: matrix of every team's service domain:port (public targets).""" + require_login(req) + out = [] + for d in sorted(orch.TEAMS_DIR.glob("team*")): + 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") + for name, coff, soff in orch.CHALLENGES: + p = st["ports"].get(name) + if not p: + continue + out.append({ + "team": st.get("index"), + "team_label": st.get("label", f"Team {st.get('index')}"), + "challenge": name, + "domain": dom, + "port": p["chall"], + }) + return {"targets": out} + # ============ WebSocket SSH terminal (team portal) ============ import paramiko diff --git a/panel/static/index.html b/panel/static/index.html index fa72225..a541d7e 100644 --- a/panel/static/index.html +++ b/panel/static/index.html @@ -115,6 +115,8 @@
--:--:-- + +
@@ -125,6 +127,7 @@ + @@ -190,6 +193,17 @@
+ +
+
+
+ 🎯 Target Matrix + Domain:port semua service semua tim — buat dibagikan ke peserta sebagai daftar target +
+
Memuat…
+
+
+ @@ -519,6 +533,28 @@ async function stopAllTeams() { finally { hideLoading(); } } +async function resetScores() { + if (!confirm('⚠️ Apakah anda yakin ingin mereset skor?\n\nSemua poin & solve di leaderboard akan DIHAPUS.\nTindakan ini tidak bisa dibatalkan.')) return; + showLoading('Menghapus semua skor…'); + try { + const d = await api('/api/reset/scores', {method:'POST', headers:{'Content-Type':'application/json'}, body: '{}'}); + toast('Skor direset: semua solve dihapus', false); + } catch (e) { toast(e.message, true); } + finally { hideLoading(); } +} + +async function resetEnv() { + if (!confirm('⚠️ Apakah anda yakin ingin mereset environment?\n\nSEMUA team akan DISTOP:\n• Container challenge dihapus\n• Receiver team dihapus\n• Folder team dihapus (perlu create ulang)\n• Skor leaderboard direset\n\nTindakan ini TIDAK BISA dibatalkan.')) return; + showLoading('Reset environment…\nMenghentikan semua team + menghapus container/receiver (beberapa menit)'); + try { + const d = await api('/api/reset/environment', {method:'POST', headers:{'Content-Type':'application/json'}, body: '{}'}); + const n = (d.stopped || []).length; + toast(`Environment direset: ${n} team dihentikan & dihapus`, false); + setTimeout(() => location.reload(), 1200); + } catch (e) { toast(e.message, true); } + finally { hideLoading(); } +} + async function randomizeTeam(idx) { if (!confirm(`Randomize SEMUA flag untuk Team ${idx}? Container akan di-recreate.`)) return; try { @@ -619,6 +655,27 @@ async function loadCreds() { } catch (e) { toast(e.message, true); } } +// ---------- Target Matrix ---------- +async function loadTargetMatrix() { + const box = document.getElementById('targetMatrix'); + try { + const d = await api('/api/targets'); + const targets = d.targets || []; + if (!targets.length) { box.textContent = 'Belum ada team.'; return; } + // group by team + const rows = []; + let curTeam = null; + for (const t of targets) { + if (t.team !== curTeam) { + rows.push(`\n[${esc(t.team_label || ('Team ' + t.team))}]`); + curTeam = t.team; + } + rows.push(` ${esc(t.domain)}:${t.port} (${esc(t.challenge)})`); + } + box.textContent = rows.join('\n'); + } catch (e) { box.textContent = 'Gagal: ' + e.message; } +} + // ---------- misc ---------- function closeModal(id) { document.getElementById(id).classList.remove('open'); } async function logout() { diff --git a/panel/static/team.html b/panel/static/team.html index 97f384e..bbf345a 100644 --- a/panel/static/team.html +++ b/panel/static/team.html @@ -264,10 +264,7 @@ async function loadTargets() { if (!targets.length) { box.textContent = 'Belum ada tim musuh.'; return; } let html = '=== TARGET MUSUH ===\n\n'; for (const t of targets) { - html += `[${esc(t.team)}] — ${esc(t.challenge)}\n`; - if (t.domain) html += ` domain : https://${esc(t.domain)}\n`; - html += ` service: ${esc(window.location.hostname)}:${t.port}\n`; - html += ` ssh : ${esc(window.location.hostname)}:${t.ssh}\n\n`; + html += `${esc(t.domain)}:${t.port}\n`; } box.textContent = html; } diff --git a/panel/teams.py b/panel/teams.py index 99f12ea..1998edd 100644 --- a/panel/teams.py +++ b/panel/teams.py @@ -402,6 +402,50 @@ def submit_flag(team_idx: int, chall: str, flag: str, team_name: str = "") -> di lb_path.write_text(json.dumps(lb, indent=2)) return {"success": True, "team": team_idx, "challenge": chall} +def reset_scores() -> dict: + """Wipe the leaderboard (all solves removed).""" + lb_path = TEAMS_DIR / "leaderboard.json" + lb_path.write_text(json.dumps({"solves": []}, indent=2)) + return {"ok": True, "cleared": True} + +def reset_environment() -> dict: + """Full env reset: stop all teams, remove containers + receiver services, + delete team dirs (fresh for re-create). Keeps panel + images.""" + results = [] + for d in sorted(TEAMS_DIR.glob("team*")): + sf = d / "state.json" + if not sf.exists(): + continue + st = json.loads(sf.read_text()) + idx = st["index"] + try: + stop_team(idx) # compose down + stop receiver service + set status + except Exception as e: + results.append({"team": idx, "ok": False, "err": str(e)}) + continue + # remove the per-team systemd receiver unit + subprocess.run(["systemctl", "disable", f"gemastik-receiver-team{idx}.service"], + check=False, capture_output=True) + subprocess.run(["systemctl", "stop", f"gemastik-receiver-team{idx}.service"], + check=False, capture_output=True) + unit = Path("/etc/systemd/system") / f"gemastik-receiver-team{idx}.service" + if unit.exists(): + unit.unlink() + results.append({"team": idx, "ok": True}) + subprocess.run(["systemctl", "daemon-reload"], check=False) + # remove team dirs entirely (fresh for re-create) + for d in sorted(TEAMS_DIR.glob("team*")): + shutil.rmtree(d, ignore_errors=True) + # wipe leaderboard too + reset_scores() + # drop team domains from Traefik (regenerate — no teams left) + try: + ensure_team_domains() + except Exception: + pass + return {"ok": True, "stopped": results, "teams_dir": str(TEAMS_DIR)} + + if __name__ == "__main__": import sys cmd = sys.argv[1] if len(sys.argv) > 1 else "list"