# Attack Defense Platform Platform attack-defense (serang–serang) untuk GEMASTIK Final Round, menyatukan **28 challenge** dari tiga set (**GEMASTIK XVIII**, **XVI**, **XVII**) di bawah satu panel admin, satu flag store, satu SLA checker, dan satu skorboard. Multi-team: N tim, masing-masing memiliki copy semua challenge + flag sendiri, dengan receiver terisolasi per tim, checker SLA otomatis, dan visualisasi topologi serangan real-time. ``` Browser (admin/team) | HTTP + WebSocket (proxy server-side) v Panel :18081 (FastAPI) ---- systemd: gemastik-panel | +--> Receiver global :18080 ---- systemd: gemastik-receiver +--> Receiver tim N :31080+1000*(N-1) ---- systemd: gemastik-receiver-teamN | (menjalankan checker SLA untuk tim N) +--> N x _container_teamN ---- docker compose | +--> flags + leaderboard + points (teams/leaderboard.json, teams/points.json) ``` --- ## Daftar Isi - [Arsitektur](#arsitektur) - [Spesifikasi Challenge](#spesifikasi-challenge) - [Spesifikasi Port](#spesifikasi-port) - [Skor dan Penilaian](#skor-dan-penilaian) - [Kebutuhan Sistem](#kebutuhan-sistem) - [Setup](#setup) - [Operasional Harian](#operasional-harian) - [Topologi](#topologi) - [HTTP API](#http-api) - [Troubleshooting](#troubleshooting) - [Jebakan Operasional](#jebakan-operasional) - [Struktur Direktori](#struktur-direktori) --- ## Arsitektur Tiga proses inti, semuanya dikelola systemd: | Proses | Port | Unit systemd | Peran | |---|---|---|---| | Panel admin | **18081** | `gemastik-panel` | Web UI admin + seluruh API | | Receiver global | **18080** | `gemastik-receiver` | Flag store untuk mode single-node | | Receiver tim N | **31080 + 1000×(N−1)** | `gemastik-receiver-teamN` | Checker SLA tim N | **Mengapa receiver per tim harus unit terpisah.** Kalau receiver dijalankan sebagai child process dari panel, `systemctl restart gemastik-panel` akan membunuh seluruh cgroup — termasuk semua receiver — dan SLA semua tim ikut turun ke 0. Unit terpisah membuat restart panel tidak menyentuh receiver. Generatornya: `panel/gen_receiver_services.py`. **Kredensial tidak pernah masuk browser.** Panel melakukan proxy ke receiver secara server-side, sehingga password admin hanya ada di `panel/.env` pada host. **Sumber data tunggal.** `teams/challenge_registry.json` dibaca oleh panel, generator compose, dan generator receiver. Menambah challenge = menambah satu entri di registry, bukan menyunting tiga tempat. ### Alur satu flag ``` tim penyerang submit flag -> panel POST /api/flag/submit -> baca flag tim target dari receiver -> cocok? ya -> catat di leaderboard + skor untuk PENYERANG tidak -> tolak ``` Flag di-mint per (tim, challenge), bukan satu flag global. --- ## Spesifikasi Challenge **28 challenge terdaftar, 16 aktif secara default.** Kolom `Port team 1` / `SSH team 1` diisi `-` untuk challenge nonaktif karena port-nya baru dialokasikan saat challenge diaktifkan. ### GEMASTIK XVIII — 6 challenge, 6 aktif User SSH: `ctfuser`. | # | Challenge | Kategori | org_port | Offset chall/SSH | Port team 1 | SSH team 1 | Status | |---|---|---|---|---|---|---|---| | 1 | `blogpost` | web | 10000 | 0/22 | 31000 | 31022 | aktif | | 2 | `carbeat` | pwn | 11000 | 1/23 | 31001 | 31023 | aktif | | 3 | `cdn` | web | 12000 | 2/24 | 31002 | 31024 | aktif | | 4 | `phew` | crypto | 13000 | 3/25 | 31003 | 31025 | aktif | | 5 | `sheesh` | crypto | 14000 | 4/26 | 31004 | 31026 | aktif | | 6 | `warmup` | warmup | 15000 | 5/27 | 31005 | 31027 | aktif | ### GEMASTIK XVI — 10 challenge, 3 aktif User SSH: `root`. | # | Challenge | Kategori | org_port | Offset chall/SSH | Port team 1 | SSH team 1 | Status | |---|---|---|---|---|---|---|---| | 7 | `art` | web | 10000 | 10/110 | 31010 | 31110 | aktif | | 8 | `xl` | web | 11000 | 11/111 | 31011 | 31111 | aktif | | 9 | `gemas-notes` | web | 12000 | 12/112 | - | - | nonaktif | | 10 | `pasta` | web | 13000 | 13/113 | - | - | nonaktif | | 11 | `burvesigner` | crypto | 14000 | 14/114 | - | - | nonaktif | | 12 | `hirnfick` | pwn | 15000 | 15/115 | - | - | nonaktif | | 13 | `gemas-fetcher` | web | 16000 | 16/116 | - | - | nonaktif | | 14 | `s3` | web | 20000 | 20/120 | 31020 | 31120 | aktif | | 15 | `crawlback` | web | 21000 | 21/121 | - | - | nonaktif | | 16 | `back-to-basic` | warmup | 22000 | 22/122 | - | - | nonaktif | ### GEMASTIK XVII — 12 challenge, 7 aktif User SSH: `root`. | # | Challenge | Kategori | org_port | Offset chall/SSH | Port team 1 | SSH team 1 | Status | |---|---|---|---|---|---|---|---| | 17 | `anti-alchemy` | web | 11000 | 30/130 | 31030 | 31130 | aktif | | 18 | `asmr` | pwn | 15000 | 31/131 | - | - | nonaktif | | 19 | `bit-canvas` | pwn | 20000 | 32/132 | 31032 | 31132 | aktif | | 20 | `fjb` | web-pwn | 17000 | 33/133 | - | - | nonaktif | | 21 | `gift-card` | crypto | 21000 | 34/134 | 31034 | 31134 | aktif | | 22 | `gift-voucher` | crypto | 16000 | 35/135 | 31035 | 31135 | aktif | | 23 | `gleam-drive` | web-crypto | 12000 | 36/136 | 31036 | 31136 | aktif | | 24 | `go-green` | rev | 20000 | 37/137 | - | - | nonaktif | | 25 | `kode-viewer` | web | 10000 | 38/138 | - | - | nonaktif | | 26 | `more-less` | web | 22000 | 39/139 | 31039 | 31139 | aktif | | 27 | `tempest-poc` | web | 14080 | 40/140 | - | - | nonaktif | | 28 | `ticketer` | crypto | 14000 | 41/141 | 31041 | 31141 | aktif | Kategori: 13 web, 6 crypto, 4 pwn, 2 warmup, dan masing-masing satu web-pwn, web-crypto, rev. `gleam-drive` dilayani lewat HTTPS (field `scheme` di registry), 27 challenge lainnya HTTP. ### Format flag ``` GEMASTIK18{TEAM__<12 hex>} contoh: GEMASTIK18{TEAM1_BLOGPOST_<12 hex acak>} ``` ### User SSH per challenge Hanya 6 challenge native GEMASTIK XVIII yang membuat user `ctfuser`. Semua challenge impor XVI/XVII menjalankan `echo root:${PASSWORD} | chpasswd` di Dockerfile, sehingga login sebagai `ctfuser` ditolak walaupun password benar. Field `ssh_user` di `teams/challenge_registry.json` yang menentukan ini, dibaca `panel/teams.py` saat `set_ssh_passwords()`. Men-hardcode `ctfuser` membuat 10 dari 16 challenge gagal login padahal `state.json` terlihat benar. --- ## Spesifikasi Port Setiap tim mendapat blok port sendiri, dengan basis 30000 dan langkah 1000: ``` port_challenge(tim i, challenge c) = 30000 + 1000 x i + chall_offset(c) port_ssh(tim i, challenge c) = 30000 + 1000 x i + ssh_offset(c) port_receiver(tim i) = 30000 + 1000 x i + 80 ``` `chall_offset` dan `ssh_offset` dibaca dari `teams/challenge_registry.json` (`panel/teams.py`, `create_team()`). Field `org_port` di registry adalah port native challenge di graveyard asalnya dan **tidak dipakai** untuk menghitung port runtime. Enam challenge native GEMASTIK XVIII memakai offset challenge 0–5 dan SSH 22–27, sehingga untuk team 1 berada di 31000–31005 dan 31022–31027: | Tim | Port challenge | Port SSH | Receiver | |---|---|---|---| | 1 | 31000–31005 | 31022–31027 | 31080 | | 2 | 32000–32005 | 32022–32027 | 32080 | | 3 | 33000–33005 | 33022–33027 | 33080 | | 4 | 34000–34005 | 34022–34027 | 34080 | Challenge impor memakai offset sendiri, jadi portnya tidak selalu berakhiran `0000`–`0005`. Angka nyata team 1: `art` 31010/31110, `xl` 31011/31111, `s3` 31020/31120, `anti-alchemy` 31030/31130, `bit-canvas` 31032/31132, `gift-card` 31034/31134, `gift-voucher` 31035/31135, `gleam-drive` 31036/31136, `more-less` 31039/31139, `ticketer` 31041/31141. Dua challenge dengan `org_port` sama tidak bentrok, karena yang dipakai adalah `chall_offset`. `anti-alchemy` (XVII, offset 30) dan `carbeat` (XVIII, offset 1) sama-sama punya `org_port` 11000, tetapi memakai port 31030 dan 31001. --- ## Skor dan Penilaian ```python POINTS_PER_FLAG = 100 # ke tim PENYERANG, hanya solve pertama SLA_BONUS_POINTS = 50 # bonus bila semua challenge aktif UP SLA_BONUS_MIN_ALIVE = 6 # konstanta; threshold runtime = len(enabled_challenges()) ``` **Attack points.** Submit flag benar milik tim lain memberi +100 ke tim penyerang. Duplikat (flag + penyerang + target sama) tidak dihitung dua kali. **SLA bonus.** Diberi bila semua challenge yang aktif UP, maksimal sekali per jendela 5 menit. Threshold runtime bukan angka tetap 6 melainkan `len(enabled_challenges())` — mengaktifkan challenge ke-17 membuat syaratnya "semua 17 UP". **Badge.** Juara, runner-up, dan tempat ketiga dihitung dari total poin. --- ## Kebutuhan Sistem Host reference: Ubuntu 24.04 (noble), x86_64, Docker + Compose v2, systemd. | Sumber daya | Minimum | Recommended | |---|---|---| | CPU | 2 vCPU | 4 vCPU | | RAM | 8 GB | 16 GB | | Disk | 60 GB | 100 GB+ | | Docker | Compose v2 (`docker compose`) | — | Compose v1 (`docker-compose`) tidak didukung — seluruh generator memakai `docker compose`. Disk adalah pembatas utama. Tiap challenge yang aktif menjadi satu image `services-`; ukurannya bervariasi dari ~190 MB (`gift-card`) sampai ~900 MB (`warmup`), dan pada host ini 16 image aktif menempati sekitar 8 GB. Membangun banyak challenge sekaligus akan mengisi disk sebelum selesai — implementasi terbaik adalah membangun challenge secara berurutan dan menjalankan `docker builder prune -af` di antaranya. `phew` menjalankan generator kunci Paillier saat start (±12 detik) sehingga butuh RAM ekstra dan checker-nya memakai `_CRYPTO_TIMEOUT`, bukan timeout prompt bawaan 5 detik. Prasyarat jaringan: setiap compose template sudah memuat `extra_hosts: host.docker.internal:host-gateway`, dan UFW host harus mengizinkan port challenge (lihat [Jebakan Operasional](#jebakan-operasional)). Dependency checker ada di `receiver/requirements.txt`: fastapi, uvicorn, pwntools, pyelftools, pycryptodome, fastecdsa, ecdsa, Pillow, pandas, openpyxl, PyPDF2. --- ## Setup ### 1. Clone ```bash git clone attack-defense-platform cd attack-defense-platform ``` Semua path di dalam kode memakai `/opt/gemastik18-final` sebagai `BASE`, jadi letakkan repo di sana: ```bash sudo mkdir -p /opt sudo mv attack-defense-platform /opt/gemastik18-final cd /opt/gemastik18-final ``` ### 2. Docker ```bash sudo bash node.sh ``` `node.sh` memasang Docker CE dari repo resmi lalu menjalankan `starter.py`. Instalasi manual: ```bash sudo apt-get install -y docker-ce docker-ce-cli containerd.io \ docker-buildx-plugin docker-compose-plugin ``` ### 3. Kredensial panel ```bash cat > panel/.env <<'EOF' PANEL_ADMIN_USER=admin PANEL_ADMIN_PASS=ganti-dengan-password-kuat EOF chmod 600 panel/.env ``` `panel/.env` sudah masuk `.gitignore` dan tidak pernah ter-commit. ### 4. Python environment ```bash cd /opt/gemastik18-final/receiver sudo python3 -m venv .venv sudo .venv/bin/pip install -r requirements.txt ``` ### 5. Unit systemd Panel (:18081): ```ini # /etc/systemd/system/gemastik-panel.service [Unit] Description=Gemastik A/D Panel (web UI for receiver) After=gemastik-receiver.service network-online.target Wants=gemastik-receiver.service [Service] Type=simple WorkingDirectory=/opt/gemastik18-final/panel EnvironmentFile=-/opt/gemastik18-final/panel/.env ExecStart=/opt/gemastik18-final/receiver/.venv/bin/python -m uvicorn main:app --host 0.0.0.0 --port 18081 Restart=always RestartSec=5 Environment=PYTHONUNBUFFERED=1 [Install] WantedBy=multi-user.target ``` Receiver global (:18080): ```ini # /etc/systemd/system/gemastik-receiver.service [Unit] Description=Gemastik18 Receiver Service (CTF flag/control API) After=docker.service network-online.target Wants=docker.service Requires=docker.service [Service] Type=simple WorkingDirectory=/opt/gemastik18-final/receiver ExecStart=/opt/gemastik18-final/receiver/.venv/bin/python -m uvicorn main:app --host 0.0.0.0 --port 18080 Restart=always RestartSec=5 Environment=PYTHONUNBUFFERED=1 [Install] WantedBy=multi-user.target ``` ```bash sudo systemctl daemon-reload sudo systemctl enable --now gemastik-receiver gemastik-panel systemctl is-active gemastik-panel gemastik-receiver ``` ### 6. Buat tim Lewat UI (**Teams** tab, admin login) atau API: ```bash curl -X POST http://127.0.0.1:18081/api/teams/set \ -H 'Content-Type: application/json' \ -b cookies.txt -c cookies.txt \ -d '{"count":2,"labels":{"1":"Tim Satu","2":"Tim Dua"}}' ``` Endpoint ini idempoten (membuat yang hilang, mempertahankan yang ada) dan otomatis menjalankan `sync_team_ufw()` untuk setiap tim baru — tanpa itu port tim akan di-blackhole UFW. ### 7. Verifikasi ```bash bash panel/verify_platform_health.py ``` --- ## Operasional Harian | Aksi | Perintah | |---|---| | Lihat status semua service | `systemctl is-active gemastik-panel gemastik-receiver gemastik-receiver-team*` | | Restart panel | `systemctl restart gemastik-panel` | | Sinkronkan container tim dengan registry | `bash panel/apply_registry.sh` | | Health check | `python3 panel/verify_platform_health.py` | | SSH round-trip ke semua challenge | `python3 panel/verify_ssh_e2e.py` | | Cek user SSH per challenge | `bash panel/audit_ssh_users.sh` | | Reset penuh (tim, flag, kredensial) | `bash panel/reset_runtime.sh` | | Health suite topologi | `bash panel/verify_topo_full.sh` | | Beban host | `bash panel/watch_load.sh` | **Reverse proxy.** Domain challenge dan panel dilayani Traefik lewat file dynamic di `/data/coolify/proxy/dynamic/attackdefense.yaml`. Pola service: ```yaml services: gemastik-panel-service: loadBalancer: servers: - url: "http://host.docker.internal:18081" ``` Cert TLS terbit otomatis lewat `certResolver: letsencrypt` selama DNS terresolve dan port 80 terbuka. Domain yang dipakai di host ini: `panel.attackdefense.imrnes.team` (panel, :18081) dan `attackdefense.imrnes.team` (receiver global, :18080), plus subdomain per challenge aktif. Perhatikan domain bare menunjuk ke receiver, bukan panel — `/login` di sana akan 404 dan terlihat seperti panel mati. --- ## Topologi Tab **Topology** merender graf serangan antar tim dengan PixiJS v8 (`panel/static/topo_pixi.js`, engine di `panel/static/vendor/pixi.mjs`). - Pan, zoom, dan drag berjalan lewat satu funnel `applyView()`. - Drag node tim menyeret seluruh challenge-nya. Indeks parent→child dibangun dari edge list — sumber yang sama untuk menggambar garis — sehingga hierarki drag tidak mungkin berbeda dari gambar. - Ticker Pixi didaftarkan tapi tidak dinyalakan: render berlangsung on demand (hanya saat ada pulse serangan atau sedang drag), lalu berhenti saat sunyi. Pada host tanpa GPU, repaint 60fps atas scene statis membuat halaman tidak merespons (rAF turun ke 2 FPS, lag `setTimeout(0)` 1353 ms). - Posisi drag kembali ke layout otomatis saat data di-refresh tiap 10 detik. Untuk merender ulang objek, `.text` hanya di-set bila string benar-benar berubah — setiap `Text` baru meng-upload texture GPU (~1,6 detik per siklus bila di-rebuild terus-menerus). Suite tes: `bash panel/run_topo_tests.sh` (`test_topo_pixels`, `test_topo_browser`, `test_topo_viewports`, `test_topo_race`, `test_topo_drag`). --- ## HTTP API Semua endpoint di `/api` kecuali yang ditandai publik. ### Publik | Method | Path | Keterangan | |---|---|---| | GET | `/submit` | UI submit flag publik | | POST | `/api/flag/submit` | Submit flag | | GET | `/api/public/scoreboard` | Skorboard tanpa login | | GET | `/api/public/teams` | Daftar tim tanpa login | ### Admin (butuh login) | Method | Path | Keterangan | |---|---|---| | POST | `/api/login` | Login admin | | POST | `/api/logout` | Logout | | GET | `/api/challenges` | Daftar challenge + status | | PATCH | `/api/challenges/{challenge}` | Toggle enable/disable (body `{"enabled":bool}`) | | GET | `/api/challenges/jobs/{job_id}` | Progress job toggle | | GET | `/api/status` | Status runtime | | GET | `/api/topology` | Data graf topologi | | GET | `/api/teams` | Daftar tim | | POST | `/api/teams/set` | Buat N tim (idempoten) | | PUT | `/api/teams/{idx}` | Ubah label/domain tim | | DELETE | `/api/teams/{idx}` | Hapus satu tim (body `{"purge_scores":true}`) | | POST | `/api/teams/bulk-delete` | Hapus beberapa tim (job) | | GET | `/api/teams/bulk-delete/{job_id}` | Progress job hapus massal | | POST | `/api/teams/{idx}/ufw` | Sinkronkan aturan UFW tim | | POST | `/api/teams/start` | Start semua tim | | POST | `/api/teams/stop` | Stop semua tim | | POST | `/api/teams/{idx}/randomize` | Acak flag tim | | GET | `/api/teams/{idx}/logs` | Log tim | | GET | `/api/teams/{idx}/creds` | Kredensial tim | | GET | `/api/credential/{challenge}` | Kredensial satu challenge | | GET | `/api/targets` | Target serangan | | GET | `/api/attacks` | Log serangan | | GET | `/api/leaderboard` | Leaderboard | | GET | `/api/scoreboard` | Skorboard internal | | GET | `/api/history` | Riwayat | | POST | `/api/restart/{challenge}` | Restart challenge | | POST | `/api/rollback/{challenge}` | Rollback challenge | | POST | `/api/activate/{challenge}` | Aktifkan challenge | | POST | `/api/deactivate/{challenge}` | Nonaktifkan challenge | | POST | `/api/reset/scores` | Reset skor | | POST | `/api/reset/environment` | Reset environment (hapus tim + flag) | Toggle challenge jalan asinkron: build per tim bisa memakan waktu menit, jadi kerjaan dijalankan di background thread dan klien melakukan polling ke `/api/challenges/jobs/{job_id}`. Hapus tim massal juga berupa job karena satu tim butuh sekitar 100 detik (`compose down` 16 service) — empat tim inline akan menahan request sekitar 7 menit dan memicu timeout di semua proxy. ### Tim (login tim) | Method | Path | Keterangan | |---|---|---| | GET | `/team/{idx}` | Portal tim | | GET | `/team/{idx}/guide` | Panduan tim | | POST | `/api/team/{idx}/login` | Login tim | | POST | `/api/team/logout` | Logout tim | | GET | `/api/team/{idx}/session` | Status sesi | | GET | `/api/team/{idx}/own-challenges` | Challenge milik tim | | GET | `/api/team/{idx}/targets` | Target untuk diserang | | GET | `/api/team/{idx}/info` | Info tim | | GET | `/api/team/{idx}/status` | Status challenge tim | | GET | `/api/team/{idx}/activity` | Aktivitas tim | | WS | `/api/team/{idx}/ssh/ws` | Terminal web ke container tim | --- ## Troubleshooting **`/login` di domain attackdefense.imrnes.team mengembalikan 404.** Domain bare diarahkan ke receiver global (:18080), bukan panel. Panel ada di `panel.attackdefense.imrnes.team` (:18081). Dua router berbeda melayani kedua subdomain di `attackdefense.yaml`. **SLA turun ke 0 padahal container hidup.** Bisa jadi receiver-nya mati, atau sering: restart panel mematikan receiver karena keduanya satu cgroup. Cek `systemctl is-active gemastik-receiver-team*`. **SSH ditolak padahal password di `state.json` benar.** Cek `ssh_user` untuk challenge tersebut di registry. 10 dari 16 challenge impor login sebagai `root`, bukan `ctfuser`. **Flag expired / tidak cocok.** `reset_environment()` menghapus flag lama. Cek `teams/team/receiver/flags/.txt`. **Waktu toggle sangat lama.** Normal — satu toggle membangun image per tim. Pantau lewat `/api/challenges/jobs/{job_id}`, bukan dengan kill prosesnya. **`pull access denied for services-`.** Image belum ada sehingga compose mencoba build dengan context yang salah. `compose_gen` hanya menukar `build` menjadi `image` bila image-nya benar-benar ada di `docker images`. **Disk penuh (`/` 0 byte).** Lihat [Jebakan Operasional](#jebakan-operasional). Yang benar: `docker builder prune -af` dan `journalctl --vacuum-size=50M`. `docker image prune -af` menghapus image `services-*` yang sedang dipakai. --- ## Jebakan Operasional Yang sudah ketahuan dan sudah diperbaiki. Semua masih berlaku sebagai alasan mengapa kode sekarang berbentuk seperti sekarang. **Base image EOL.** `debian:buster`, `ubuntu:20.04`, dan `node:14` gagal `apt-get update` karena GPG kedaluwarsa atau mirror 404. Pakai bookworm/noble, `node:20`. **UFW default deny.** Host ini punya UFW aktif default deny. Port baru harus dibuka (`ufw allow /tcp`) atau traffic di-blackhole diam-diam — termasuk dari container lewat docker bridge. `POST /api/teams/set` sudah menjalankan `sync_team_ufw()` karena alasan ini. **Project name compose wajib.** Per-team compose harus dijalankan dengan `-p teamN`. Tanpa itu `docker compose` memakai nama direktori induk (`services`) untuk semua tim, sehingga container team2 tertimpa team1. **`docker image prune -af` menghapus image yang sedang dipakai.** Image `services-*` menjadi dangling dan terhapus meski container masih jalan. Container tetap hidup tetapi image hilang dan tidak bisa di-recreate. Untuk membersihkan ruang: `docker builder prune -af` + `journalctl --vacuum-size=100M` + hapus `/root/.cache`. **Container orphan.** Sweep dengan: ```bash docker ps --format '{{.Names}}' | grep -E '_container$' | grep -vE '_team[0-9]+$' ``` **Beban checker.** Host 2-CPU/8GB tidak boleh meng-probe 4 receiver sekaligus (Flask sinkron + CPU bersama = SLA timeout palsu), dan tidak boleh menjalankan banyak generator kunci Paillier/RSA bersamaan. Cek `uptime`, `free -m`, `vmstat 1 3` sebelum menyalahkan checker. **`subprocess.run(['docker','exec',...])` tanpa timeout.** Container yang jenuh memblokir selamanya dan menahan seluruh loop SLA. --- ## Struktur Direktori ``` /opt/gemastik18-final/ ├── README.md ├── node.sh # installer Docker ├── starter.py # bootstrap single-node (upstream) ├── teams/ │ ├── challenge_registry.json # sumber data tunggal 28 challenge │ ├── points.json # skor + event │ ├── leaderboard.json # solve │ ├── attacks.json # log serangan (visualisasi topologi) │ └── teamN/ │ ├── state.json # port, flag, kredensial, label, domain │ ├── services/docker-compose.yml # hasil generate per tim │ └── receiver/ # receiver terisolasi tim N ├── services// # Dockerfile + compose template (28 challenge) ├── panel/ │ ├── main.py # FastAPI: seluruh route │ ├── teams.py # orkestrasi tim, port, flag, skor, SLA │ ├── compose_gen.py # render compose per tim dari registry │ ├── gen_receiver_services.py # generate unit systemd per tim │ ├── gen_receiver_main.py # generate main.py receiver per tim │ ├── apply_registry.sh # sinkronkan container dengan registry │ ├── reset_runtime.sh # reset penuh │ ├── verify_platform_health.py │ ├── verify_ssh_e2e.py │ ├── audit_ssh_users.sh │ ├── run_topo_tests.sh │ ├── verify_topo_full.sh │ └── static/ │ ├── index.html # dashboard admin │ ├── team.html # portal tim │ ├── topo_pixi.js # renderer topologi PixiJS v8 │ └── vendor/pixi.mjs └── receiver/ ├── main.py # API receiver (flag store + checker) ├── config.py ├── requirements.txt ├── challenges/ # checker: Blogpost, Phew, xvi/, xvii/ ├── flags/ # flag default └── .venv/ ``` --- ## Credit Challenge berasal dari tiga repositori: [gemastik18-final](https://github.com/rayhanhanaputra/gemastik18-final), [gemastik-xvi-final](https://github.com/vidner/gemastik-xvi-final), [gemastik-xvii-final](https://github.com/vidner/gemastik-xvii-final).