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
2026-09-25 14:04:33 +08:00
parent c35b23a37f
commit c6fd9ec268
1317 changed files with 306586 additions and 128 deletions
+68 -6
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Gemastik A/D Panel</title>
<title>Attack Defense Platform</title>
<style>
* { margin:0; padding:0; box-sizing:border-box; }
body {
@@ -117,7 +117,7 @@
</head>
<body>
<header>
<h1>⚔️ GEMASTIK A/D — NODE CONTROL</h1>
<h1>⚔️ ATTACK DEFENSE PLATFORM — NODE CONTROL</h1>
<div class="actions">
<span class="pill" id="clock">--:--:--</span>
<button onclick="refresh()">🔄 Refresh</button>
@@ -135,6 +135,7 @@
<button class="tab" data-view="creds" onclick="showView('creds'); loadCreds()">🔑 Creds</button>
<button class="tab" data-view="targets" onclick="showView('targets'); loadTargetMatrix()">🎯 Target Matrix</button>
<button class="tab" data-view="sla" onclick="showView('sla'); loadSla()">📊 SLA & Skor</button>
<button class="tab" data-view="chmgr" onclick="showView('chmgr'); loadChMgr()">🏗️ Challenge Manager</button>
</div>
<!-- Challenges view -->
@@ -229,7 +230,19 @@
</div>
</div>
<div class="footer-note">Receiver: https://gemastik.imrnes.team · Panel: https://panel.gemastik.imrnes.team · Auto-refresh tiap 15 detik</div>
<!-- Challenge Manager view -->
<div class="view" id="view-chmgr">
<div class="card">
<div style="display:flex;align-items:center;gap:10px;flex-wrap:wrap">
<b style="color:#5ad1ff">🏗️ Challenge Manager</b>
<span style="font-size:11px;color:#718096">Semua challenge dari registry (GEMASTIK 18, XVI, XVII). Toggle untuk aktif/nonaktif global di semua tim — diterapkan langsung (build/up atau stop/remove + restart receiver).</span>
<button class="primary" onclick="loadChMgr()" style="padding:4px 10px;font-size:12px">🔄 Muat ulang</button>
</div>
<div id="chMgrList" style="margin-top:14px;font-family:ui-monospace,monospace;font-size:12px;line-height:1.9;color:#dbe6f4;background:#0a101f;border:1px solid #1e3a5f;border-radius:10px;padding:14px;overflow-x:auto">Memuat…</div>
</div>
</div>
<div class="footer-note">Receiver: https://attackdefense.imrnes.team · Panel: https://panel.attackdefense.imrnes.team · Auto-refresh tiap 15 detik</div>
<!-- modal flag -->
<div class="modal-back" id="modalFlag">
@@ -310,6 +323,7 @@ function showView(v) {
document.querySelectorAll('.view').forEach(x => x.classList.toggle('active', x.id === 'view-' + v));
if (v === 'topo') loadTopo(); // refresh attacks immediately on tab switch + every 10s
if (v === 'sla') loadSla();
if (v === 'chmgr') loadChMgr();
}
let topoTimer = null;
function startTopoTimer() {
@@ -347,6 +361,54 @@ async function loadSla() {
}
}
// ---------- Challenge Manager ----------
async function loadChMgr() {
const box = document.getElementById('chMgrList');
if (!box) return;
box.innerHTML = 'Memuat…';
try {
const d = await api('/api/challenges');
const list = d.challenges || [];
const setLabels = { 'gemastik18': 'GEMASTIK 18', 'xvi': 'GEMASTIK XVI', 'xvii': 'GEMASTIK XVII' };
const bySet = {};
for (const c of list) (bySet[c.set] = bySet[c.set] || []).push(c);
let html = '';
for (const [set, chs] of Object.entries(bySet)) {
html += `<div style="margin:8px 0 4px;color:#5ad1ff;font-weight:700">📦 ${escapeHtml(setLabels[set] || set)} (${chs.length})</div>`;
for (const c of chs) {
const on = c.enabled ? 'checked' : '';
html += `<div style="display:flex;align-items:center;gap:10px;padding:6px 8px;border-bottom:1px solid #13233d;border-radius:6px">
<input type="checkbox" id="chmgr-${escapeHtml(c.name)}" ${on} onchange="toggleChallenge('${escapeHtml(c.name)}', this.checked)" style="width:16px;height:16px;accent-color:#22c55e">
<b style="min-width:150px">${escapeHtml(c.name)}</b>
<span style="color:#718096;font-size:11px">${escapeHtml(c.set)} · ${escapeHtml(c.category || '-')}</span>
<span style="color:#3d5a80;font-size:11px">chall+${c.chall_offset}/ssh+${c.ssh_offset}</span>
<span style="margin-left:auto;font-size:11px;color:${c.enabled ? '#22c55e' : '#e74c3c'}">${c.enabled ? '● AKTIF' : '○ NONAKTIF'}</span>
<span id="chmgr-msg-${escapeHtml(c.name)}" style="font-size:11px;color:#718096"></span>
</div>`;
}
}
box.innerHTML = html || '<div style="color:#718096">Tidak ada challenge di registry.</div>';
} catch (e) {
box.innerHTML = `<div style="color:#e74c3c">Gagal memuat: ${escapeHtml(e.message)}</div>`;
}
}
async function toggleChallenge(name, enabled) {
const msg = document.getElementById('chmgr-msg-' + name);
if (!msg) return;
msg.textContent = '⏳ menerapkan…';
msg.style.color = '#f5c542';
try {
const d = await api('/api/challenges/' + encodeURIComponent(name), { method: 'PATCH', body: JSON.stringify({ enabled }) });
msg.textContent = `✓ ${d.enabled ? 'diaktifkan' : 'dinonaktifkan'} (${(d.applied || []).length} tim)`;
msg.style.color = d.enabled ? '#22c55e' : '#e74c3c';
setTimeout(() => { msg.textContent = ''; loadChMgr(); }, 4000);
} catch (e) {
msg.textContent = '✗ ' + e.message;
msg.style.color = '#e74c3c';
loadChMgr();
}
}
// ---------- Challenges ----------
async function refresh() {
const grid = document.getElementById('grid');
@@ -421,7 +483,7 @@ async function viewCred(ch) {
const c = await api(`/api/credential/${ch}`);
document.getElementById('mcTitle').textContent = 'SSH Credentials — ' + ch;
const sshPort = {blogpost:10022, carbeat:11022, cdn:12022, phew:13022, sheesh:14022, warmup:15022}[ch] || 10022;
const cmd = `ssh ctfuser@${ch}.gemastik.imrnes.team -p ${sshPort}`;
const cmd = `ssh ctfuser@${ch}.attackdefense.imrnes.team -p ${sshPort}`;
document.getElementById('mcBody').innerHTML =
`<div>User: <b>ctfuser</b></div>
<div>Pass: <b>${esc(c.password)}</b></div>
@@ -686,7 +748,7 @@ function teamCountChanged() {
<span style="color:#718096;font-size:12px;width:60px">Team ${i}</span>
<input data-idx="${i}" data-field="name" placeholder="Nama team (contoh: Cyber Warriors)" style="flex:1;min-width:120px">
<input data-idx="${i}" data-field="domain" placeholder="domain custom (contoh: team-cyber)" style="flex:1;min-width:120px">
<span style="color:#3b4a63;font-size:11px">.gemastik.imrnes.team</span>
<span style="color:#3b4a63;font-size:11px">.attackdefense.imrnes.team</span>
</div>`;
}
box.innerHTML = html;
@@ -795,7 +857,7 @@ async function viewTeamCred(idx) {
for (const [name, p] of Object.entries(t.team.ports)) {
if (name === 'receiver' || name === 'panel') continue;
html += `<div class="kv" style="margin-top:6px">
<b>${name}</b><span>ssh ctfuser@${name}.gemastik.imrnes.team -p ${p.ssh} &nbsp;·&nbsp; pass: ${esc(t.team.chall_passwords[name])}</span>
<b>${name}</b><span>ssh ctfuser@${name}.attackdefense.imrnes.team -p ${p.ssh} &nbsp;·&nbsp; pass: ${esc(t.team.chall_passwords[name])}</span>
</div>`;
}
document.getElementById('mtcTitle').textContent = `Kredensial Team ${idx}`;
+2 -2
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Gemastik A/D Panel — Login</title>
<title>Attack Defense Platform — Login</title>
<style>
* { margin:0; padding:0; box-sizing:border-box; }
body {
@@ -34,7 +34,7 @@
</head>
<body>
<div class="card">
<h1>⚔️ GEMASTIK A/D</h1>
<h1>⚔️ ATTACK DEFENSE</h1>
<div class="sub">Node Control Panel — imrnes</div>
<form id="f">
<label>Username</label>
+1 -1
View File
@@ -41,7 +41,7 @@
<div class="logo">G</div>
<div>
<h1>Gemastik XVIII — Attack &amp; Defense</h1>
<div class="sub">Submit flag untuk tim kamu. Server: gemastik.imrnes.team</div>
<div class="sub">Submit flag untuk tim kamu. Server: attackdefense.imrnes.team</div>
</div>
</header>
+1 -1
View File
@@ -4,7 +4,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="team-id" content="0">
<title>Portal Tim — Gemastik A/D</title>
<title>Portal Tim — Attack Defense Platform</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm@5.3.0/css/xterm.min.css">
<script src="https://cdn.jsdelivr.net/npm/xterm@5.3.0/lib/xterm.min.js"></script>
<style>