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 committed 2026-09-25 14:04:33 +08:00
1 parent c35b23a37f
commit c6fd9ec268
1317 files changed
+306586 -128

No files matched your search

+83
View File
@@ -0,0 +1,83 @@
class ClauseBuilder:
def __init__(self) -> None:
self._queries = []
self._chars_to_sanitize = ["'", '"']
self._is_where_called = False
def _sanitize(self, q) -> str:
for c in self._chars_to_sanitize:
if c in q:
q = q.replace(c, c * 2)
return str(q).strip()
def _where(self, column, value, op) -> map:
if not self._is_where_called:
op = "WHERE"
self._is_where_called = True
return map(self._sanitize, [column, value, op])
def final(self) -> str:
q = " ".join(self._queries)
self.__init__()
return str(q).strip()
def order_by(self, column, value) -> None:
column, value = map(self._sanitize, [column, value])
self._queries.append("ORDER BY")
self._queries.append('"' + column + '"')
self._queries.append(value)
def select(self, table) -> None:
table = self._sanitize(table)
self._queries.append("SELECT")
self._queries.append("*")
self._queries.append("FROM")
self._queries.append('"' + table + '"')
def where(self, column, value, cmp="ILIKE", op="AND") -> None:
column, value, op = self._where(column, value, op)
self._queries.append(op)
self._queries.append('"' + column + '"')
if column == "id":
self._queries.append("=")
self._queries.append(str(int(value)))
elif cmp == "EQ":
self._queries.append("=")
self._queries.append("'" + value + "'")
else:
self._queries.append("ILIKE")
self._queries.append("'%" + value + "%'")
class QueryBuilder:
def __init__(self) -> None:
self._cb = ClauseBuilder()
self._obj = {}
self._defined_keys = ["table", "columns"]
self._sorting_values = ["asc", "desc"]
self._login_keys = ["username"]
def _order_by(self) -> None:
for value, column in self._obj.items():
if value in self._sorting_values:
self._cb.order_by(column, value)
break
def _select(self) -> None:
self._cb.select(self._obj["table"])
def _where(self) -> None:
for column, value in self._obj.items():
if column in self._defined_keys + self._sorting_values:
continue
elif column in self._login_keys:
self._cb.where(column, value, "EQ")
else:
self._cb.where(column, value)
def generate(self, obj) -> str:
self._obj = obj
self._select()
self._where()
self._order_by()
return self._cb.final()
+119
View File
@@ -0,0 +1,119 @@
from flask import Flask, render_template, session, redirect, url_for
from os import environ
from antialchemy import *
from helper import *
app = Flask(__name__)
app.config["SECRET_KEY"] = environ.get("SECRET_KEY", "SECRET_KEY")
app.config["PERMANENT_SESSION_LIFETIME"] = 3 * 60
qb = QueryBuilder()
@app.get("/api/flag")
@check_login_status
def flag():
if session["user"] == "admin":
try:
return make_resp(200, open("/flag.txt").read())
except:
return make_resp(500, "Flag not found, please contact problem setter")
return make_resp(401, "You need to log in as admin to get the flag")
@app.post("/api/login")
@check_request_body
def login(*args, **kwargs):
payload = kwargs.copy()
try:
conn = create_db_conn()
cur = conn.cursor()
cur.execute(
qb.generate(
{
"table": "users",
"username": payload["username"],
}
)
)
row = cur.fetchone()
if not row:
return make_resp(401, "Invalid username/password")
_, username, password_hash = row
password = payload.pop("password")
cur.execute(
qb.generate(
{
"table": "salt",
"username": username,
}
)
)
row = cur.fetchone()
if not row:
return make_resp(401, "Invalid username/password")
_, _, salt = row
if not check_password_hash(password, salt, password_hash):
return make_resp(401, "Invalid username/password")
session.clear()
session["user"] = username
return redirect(url_for("index"))
except Exception as e:
print(f"Exception: {e}")
return make_resp(400, "Bad Request")
finally:
cur.close()
conn.close()
@app.get("/api/logout")
@check_login_status
def logout():
session.clear()
return redirect(url_for("index"))
@app.post("/api/view")
@check_login_status
@check_request_body
def view(*args, **kwargs):
payload = kwargs.copy()
try:
conn = create_db_conn()
cur = conn.cursor()
cur.execute(qb.generate(payload | {"table": "cwe"}))
rows = cur.fetchall()
return make_resp(200, "OK", {"rows": rows})
except Exception as e:
print(f"Exception: {e}")
return make_resp(400, "Bad Request")
finally:
cur.close()
conn.close()
@app.get("/")
def index():
try:
if session["user"]:
return render_template("dashboard.html")
except:
pass
return render_template("login.html")
if __name__ == "__main__":
app.run(debug=True)
+73
View File
@@ -0,0 +1,73 @@
from flask import request, session
from functools import wraps
from hashlib import sha1
from json import dumps
from os import environ
from psycopg2 import connect
from string import printable
from time import sleep
def create_db_conn():
while True:
try:
return connect(
database=environ.get("DB_NAME", "postgres"),
user=environ.get("DB_USER", "postgres"),
password=environ.get("DB_PASS", "password"),
host=environ.get("DB_HOST", "localhost"),
port=environ.get("DB_PORT", "5432"),
)
except:
sleep(1)
def make_resp(status, message, data=None):
return {"status": status, "message": message, "data": data}
def generate_password_hash(password, salt):
return sha1((salt + password).encode()).hexdigest()
def check_password_hash(password, salt, password_hash):
return generate_password_hash(password, salt) == password_hash
def check_login_status(f):
@wraps(f)
def inner(*args, **kwargs):
try:
session["user"]
return f(*args, **kwargs)
except Exception as e:
print(f"Exception: {e}")
return make_resp(401, "Unauthorized")
return inner
def check_request_body(f):
check_blist = lambda s: all(x not in s.lower() for x in ["pg_"])
check_wlist = lambda s: all(c in printable for c in s)
@wraps(f)
def inner(*args, **kwargs):
try:
data = request.get_json()
data = {k: v for k, v in data.items() if data.get(k)}
dd = dumps(data, separators=(",", ":"))
assert len(dd) < 256 and check_blist(dd), "Bad payload"
for item in data.items():
assert all(map(check_wlist, item)), "Bad payload"
kwargs.update(data)
return f(*args, **kwargs)
except Exception as e:
print(f"Exception: {e}")
return make_resp(400, "Bad Request")
return inner
@@ -0,0 +1,28 @@
import sys
sys.path += [".", ".."]
from helper import create_db_conn
from os import environ
print("Getting environment variables...")
print(environ.get("SECRET_KEY", "SECRET_KEY"))
print()
print("Getting rows of users and salt...")
conn = create_db_conn()
cur = conn.cursor()
cur.execute("SELECT * FROM users")
rows = cur.fetchall()
for row in rows:
print(row)
cur.execute("SELECT * FROM salt")
rows = cur.fetchall()
for row in rows:
print(row)
print()
print("Done")
cur.close()
conn.close()
@@ -0,0 +1,25 @@
import sys
sys.path += [".", ".."]
from helper import create_db_conn, generate_password_hash
from os import environ, urandom
conn = create_db_conn()
cur = conn.cursor()
users = [
("admin", environ.get("SECRET_KEY", "SECRET_KEY"), urandom(8).hex()),
("gemastik", "P@ssw0rd", urandom(8).hex()),
]
for username, password, salt in users:
cur.execute(
"INSERT INTO users (username, password) VALUES (%s, %s)",
(username, generate_password_hash(password, salt)),
)
cur.execute("INSERT INTO salt (username, salt) VALUES (%s, %s)", (username, salt))
conn.commit()
cur.close()
conn.close()
+1
View File
@@ -0,0 +1 @@
.sortable-column,button,strong{cursor:pointer}a,strong{color:#44509e}#pagination,#search{border:var(--debug);gap:4px;width:300px;padding:4px}*{box-sizing:border-box}body{font-family:Arial,Helvetica,sans-serif}table{width:100%;border-collapse:collapse}td,th{padding:.5rem;text-align:left;border-bottom:1px solid #ddd}td{height:6rem}a{text-decoration:none;font-weight:700}button:disabled{cursor:not-allowed}.container{height:inherit;display:flex;flex-direction:column;justify-content:space-between;borLine truncated
+1
View File
@@ -0,0 +1 @@
const step=10;var rawResp={},currentData=[],totalData=[],currentPage=0,totalPage=Math.floor((rawResp.length+9)/10),pload={};function fillBody(e,t){let a=document.getElementById("fillable-body");a.innerHTML="",e.forEach((e,n)=>{let r=document.createElement("tr"),l=document.createElement("td"),i=document.createElement("td"),c=document.createElement("td"),o=document.createElement("td"),s=document.createElement("a");l.textContent=n+t,s.href=`https://cwe.mitre.org/data/definitions/${e[0]}.html`,s.teLine truncated
+1
View File
@@ -0,0 +1 @@
.form-group button{width:100%}.container{max-width:640px;position:absolute;top:50%;left:50%;-ms-transform:translate(-50%,-50%);transform:translate(-50%,-50%)}h1{text-align:center}
+1
View File
@@ -0,0 +1 @@
document.getElementById("form-submit").addEventListener("click",async function(e){e.preventDefault();let t=document.getElementById("form-username").value,a=document.getElementById("form-password").value;if(!t||!a){alert("Username/password cannot be empty");return}try{let n=await fetch("/api/login",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({username:t,password:a})}),o=await n.json();alert(o.message)}catch(r){console.log(r),window.location.reload()}});
@@ -0,0 +1,61 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>List of Top CWE - Common Weakness Enumeration</title>
<link rel="icon" href="data:," />
<link rel="stylesheet" href="{{ url_for('static', filename='dashboard.min.css') }}" />
</head>
<body>
<div class="container">
<div class="atas">
<form action="javascript:search()" id="search">
<label for="code">CWE ID<input type="number" name="CWE ID" id="cwe-id" min="0" autofocus /></label>
<label for="title">Title<input type="text" name="Title" id="title" maxlength="255" /></label>
<label for="description">Description<input type="text" name="Description" id="description"
maxlength="255" /></label>
<button type="submit">Search</button>
</form>
<div style="margin: auto; text-align: center;">
<p>
Welcome to <span style="font-weight: bold">List of Top CWE</span>,
{{ session["user"] }}!
</p>
<p>
Click here to access admin <strong id="access-flag">flag</strong> or
<strong id="logout">logout</strong>.
</p>
</div>
<form id="pagination">
<button type="submit" id="prev-page">Prev</button>
<p id="page"></p>
<button type="submit" id="next-page">Next</button>
</form>
</div>
<div class="bawah">
<table class="sortable">
<thead>
<tr>
<th>#</th>
<th class="sortable-column" onclick="javascript:sort(0)">
CWE ID
</th>
<th class="sortable-column" onclick="javascript:sort(1)">
Title
</th>
<th class="sortable-column" onclick="javascript:sort(2)">
Description
</th>
</tr>
</thead>
<tbody id="fillable-body"></tbody>
</table>
</div>
</div>
</body>
<script src="{{ url_for('static', filename='dashboard.min.js') }}"></script>
</html>
@@ -0,0 +1,34 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Login - Common Weakness Enumeration</title>
<link rel="icon" href="data:," />
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous" />
<link rel="stylesheet" href="{{ url_for('static', filename='login.min.css') }}">
</head>
<body>
<div class="container">
<h1>Login</h1>
<form>
<div class="form-group pt-3">
<input type="text" class="form-control" id="form-username" placeholder="Username" />
</div>
<div class="form-group pt-3">
<input type="password" class="form-control" id="form-password" placeholder="Password" />
</div>
<div class="form-group pt-3">
<button type="submit" class="btn btn-primary" id="form-submit">
Submit
</button>
</div>
</form>
</div>
<script src="{{ url_for('static', filename='login.min.js') }}"></script>
</body>
</html>