--- title: CyberArena Backend emoji: 🛡️ colorFrom: purple colorTo: blue sdk: docker app_port: 7860 pinned: false license: mit --- # CyberArena — Backend > FastAPI backend for the CyberArena cybersecurity training platform. > Single source of truth: this directory. The old `Apex/backend/` has > been removed. ## Quick start ```bash cd CyberArena pip install -r requirements.txt python main.py # serves on http://localhost:8090 ``` The server loads `.env` automatically from this folder. ## Project layout ``` CyberArena/ ├── main.py ← 5-line uvicorn entry ├── requirements.txt ├── Dockerfile ├── start.cmd │ ├── app/ ← all real code │ ├── main.py ← FastAPI app, middleware, startup │ ├── types.py ← Pydantic request models │ │ │ ├── core/ ← constants + pure helpers (no I/O) │ │ ├── config.py ← env-loaded handles (Supabase, AI URLs, keys) │ │ ├── constants.py ← topic catalog, fallback HTML, cert config │ │ ├── security.py ← normalize_vuln_key, ip/ioc/time matchers │ │ ├── text.py ← parse_json_safe (LLM JSON parser) │ │ └── module_router.py ← module → challenge_type dispatcher │ │ │ ├── services/ ← business logic / I/O │ │ ├── supabase_service.py ← PostgREST client (CRUD on pool tables) │ │ ├── challenge_loader.py ← row → TrainingData mappers (per type) │ │ ├── scenario_service.py ← Groq scenario + challenge generation │ │ ├── evaluator.py ← /evaluate handlers (5 types) │ │ ├── completion_service.py ← user_completions idempotent tracker │ │ ├── certificate_service.py ← PDF build + cert issue/verify/download │ │ └── file_storage.py ← write downloadable files to disk │ │ │ ├── generators/ ← per-type AI pool generators │ │ ├── __init__.py ← REGISTRY: list of (slug, module, teams) │ │ ├── crypto.py ← owns encryption_challenges │ │ ├── code_fixing.py ← owns code_fixing_challenges │ │ ├── log_analysis.py ← owns log_analysis_challenges │ │ └── vulnerability_hunter.py ← owns vulnerability_hunter_challenges │ │ │ ├── sandbox/ ← in-process OS simulator sandbox │ │ ├── workdir.py ← per-challenge tempdir + safe_join │ │ ├── tools.py ← built-in cat/ls/sha256sum/base64/… │ │ └── terminal.py ← shlex parse + whitelist subprocess run │ │ │ └── api/ ← FastAPI routers (HTTP shell only) │ ├── auth.py ← /api/auth │ ├── xp.py ← /api/xp │ ├── leaderboard.py ← /api/leaderboard │ ├── certificates.py ← /api/certificates* │ ├── training.py ← /api/training/* │ ├── terminal.py ← /api/training/terminal* │ └── onevone.py ← /api/onevone/* (head-to-head matches) │ ├── db/ ← all database concerns │ ├── schema/ ← numbered, idempotent SQL migrations │ │ ├── 000_extensions.sql │ │ ├── 001_users.sql │ │ ├── 002_encryption_challenges.sql │ │ ├── 004_code_fixing_challenges.sql │ │ ├── 005_log_analysis_challenges.sql │ │ ├── 006_vulnerability_hunter_challenges.sql │ │ ├── 007_onevone.sql │ │ ├── 008_certificates.sql │ │ ├── 009_user_completions.sql │ │ ├── 010_challenge_files_bucket.sql │ │ ├── 011_challenge_type_normalization.sql │ │ └── 012_onevone_vuln_hunter.sql │ ├── seed/ ← per-type example JSON (template) │ │ ├── README.md │ │ ├── crypto.example.json │ │ ├── web.example.json │ │ ├── code_fixing.example.json │ │ ├── log_analysis.example.json │ │ └── vulnerability_hunter.example.json │ ├── apply.py ← apply every schema/*.sql in order │ └── README.md │ ├── scripts/ ← ops utilities │ ├── check_state.py ← one-page DB health report │ └── seed_pools.py ← pool size + status (read-only by default) │ ├── .env ← secrets (never commit) └── README.md > **No local file storage.** AI-generated challenge files are uploaded > to the `challenge-files` Supabase Storage bucket (see > `db/schema/010_challenge_files_bucket.sql`) and the public URL is > returned in the training payload. The backend never writes to its > own disk. ``` ## API surface The FastAPI app exposes **28 routes** under `/api/`. The full list lives in `app/main.py`; the most important are: | Method | Path | Notes | |--------|-------------------------------------|-------| | POST | `/api/auth` | proxied to Supabase `apex-auth` edge function | | POST | `/api/xp` | proxied to Supabase `apex-xp` edge function | | GET | `/api/leaderboard` | top users by XP | | GET | `/api/training/list` | dashboard cards | | POST | `/api/training/generate` | hydrate cached scenario into a full challenge | | POST | `/api/training/evaluate` | legacy red+blue AI evaluator | | POST | `/api/training/evaluate-web` | 3-layer web exploitation validator | | POST | `/api/training/evaluate-code-fix` | AI code-fix grader | | POST | `/api/training/evaluate-log-analysis` | 4-field exact match + Mistral feedback | | POST | `/api/training/evaluate-vulnerability-hunter` | exact canonical-key match | | POST | `/api/training/terminal*` | OS simulator sandbox | | POST | `/api/certificates` | list / issue | | GET | `/api/certificates/{id}/pdf` | streamed PDF | | GET | `/api/certificates/verify/{code}` | public QR landing | | GET | `/api/certificates/progress` | (completions, required) | | POST | `/api/onevone/rooms` | 1v1 mode (see `app/api/onevone.py`) | ## Pool architecture Each challenge type owns its own table + its own generator. The orchestrator in `app/main.py::populate_pool_background` walks `app.generators.REGISTRY` and starts one watcher per (type, team). Pool model: `POOL_TARGET / POOL_THRESHOLD / POOL_BATCH` are module constants inside each generator. The watcher refills the table when the count drops to `POOL_THRESHOLD` and inserts `POOL_BATCH` new rows in parallel via `asyncio.gather`. See `../AGENTS.md` "Pool Architecture" for the full contract. ## Database Apply the migrations to Supabase with: ```bash python db/apply.py # needs SUPABASE_SERVICE_KEY in .env ``` Each `.sql` file is idempotent (`CREATE … IF NOT EXISTS`, `DROP POLICY IF EXISTS + CREATE`). See `db/README.md` for details. ## Running tests There is no formal test suite yet. Use: ```bash python -c "from app.main import app; print(len(app.routes), 'routes')" python scripts/check_state.py # requires SUPABASE_URL ``` ## Deployment The provided `Dockerfile` pins `python:3.11-slim` and starts the server on `PORT` (default 7860, matches Hugging Face Spaces). ### Hugging Face Spaces (Docker SDK) 1. Create a new Space → **Docker** SDK. 2. Push this folder (`CyberArena/`) to the Space's git remote: ```bash cd CyberArena git remote add hf https://huggingface.co/spaces/Alpha-Team/CyberArena git push hf main ``` 3. In the Space's **Settings → Repository secrets**, add every key the backend needs. HF injects them as environment variables (the loader in `app/_env.py` does **not** override process env, so secrets take priority over any `.env` shipped in the repo). | Secret | Required | |---|---| | `SUPABASE_URL` | yes | | `SUPABASE_ANON_KEY` | yes | | `CLOUDFLARE_API_TOKEN` | yes (primary AI) | | `CLOUDFLARE_ACCOUNT_ID` | yes | | `CLOUDFLARE_MODEL` | optional (default `@cf/qwen/qwen2.5-coder-32b-instruct`) | | `GROQ_API_KEY` | yes (fallback AI) | | `GROQ_MODEL` | optional (default `llama-3.1-8b-instant`) | | `NVIDIA_API_KEY` | yes (tertiary AI) | | `NVIDIA_MODEL` | optional (default `deepseek-ai/deepseek-v4-pro`) | | `MISTRAL_API_KEY` | yes (quaternary AI + vuln-hunter grader) | | `MISTRAL_MODEL` | optional (default `mistral-small-latest`) | 4. The Space URL will be `https://-.hf.space`. The frontend's `VITE_API_URL` must be set to that URL + `/api` (e.g. `https://alpha-team-cyberarena.hf.space/api`). 5. **Workdir caveat**: per-challenge user scripts (`solve.py`, …) live in `tempfile.mkdtemp()` on the container's `/tmp`. The container's disk is **lost on Space sleep / restart**, but challenge data and user XP live in Supabase and persist. 6. The Space can sleep after 48h of inactivity. To stay always-on, upgrade the Space's hardware tier or wake it manually before class. > The `.env` file is **never** uploaded — it's listed in > `.dockerignore`. The HF secrets panel is the only source of > environment variables in production.