draggable topology + attack visualizer + tools in containers

- topology nodes draggable (pointer events, SVG transform), layout hint shown
- attack visualizer: /api/attacks logs attacker->target events; red pulsing
  dashed arcs on recent attacks (60s hot), ⚔ counts ok/fail
- submit_flag now takes attacker_idx vs target_idx (A/D semantics); UI has
  target dropdown (enemy teams), leaderboard records target
- containers get vim+curl+wget+netcat+git+pip3 (Dockerfiles blogpost/cdn/
  phew/sheesh/warmup); warmup base ubuntu:20.04 EOL -> 24.04
- team portal: target dropdown refreshed after login (was empty pre-auth)
This commit is contained in:
root
2026-09-23 18:05:44 +08:00
parent 44dc1ac852
commit 24dbf0a662
9 changed files with 196 additions and 23 deletions
+18 -3
View File
@@ -223,6 +223,8 @@ async def api_team_targets(idx: int, req: Request):
if not p:
continue
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"),
"port": p["chall"],
})
@@ -521,9 +523,11 @@ async def api_reset_environment(req: Request):
@app.post("/api/flag/submit")
async def api_flag_submit(req: Request):
"""Public endpoint: teams submit flags. No login needed."""
"""Public endpoint: teams submit flags. No login needed.
body: {team: attacker, target?: victim, challenge, flag}"""
data = await req.json()
team = int(data.get("team", 0))
team = int(data.get("team", 0)) # attacker (tim yang submit)
target = int(data.get("target") or 0) or team # victim team (flag dicuri dari sini)
chall = data.get("challenge", "")
flag = data.get("flag", "").strip()
team_name = data.get("team_name", "").strip()[:64] or f"Team {team}"
@@ -533,7 +537,18 @@ async def api_flag_submit(req: Request):
return {"success": False, "error": "Challenge not found"}
if not flag:
return {"success": False, "error": "Flag is required"}
return orch.submit_flag(team, chall, flag, team_name)
return orch.submit_flag(target, chall, flag, attacker_idx=team, attacker_name=team_name)
@app.get("/api/attacks")
async def api_attacks(req: Request):
"""Admin: recent attack events (attacker -> target) for the topology visualizer."""
require_login(req)
ap = orch.TEAMS_DIR / "attacks.json"
if ap.exists():
log = json.loads(ap.read_text())
else:
log = {"events": []}
return {"events": log.get("events", [])}
@app.get("/api/leaderboard")