# CTF challenge status tracker Tracks the progress of the h4tum CTF challenges. It reads every repo in the GitLab challenges group, works out each challenge's status, category and difficulty from its `README.md`, and checks that challenges marked as ready really are. The result goes to: - **a HedgeDoc note**: an overview of the challenges included in the CTF, refreshed every 30 minutes - **`challs.txt` in the deployment repo**: the clone URLs of every challenge included in the CTF and ready for infra testing, updated daily - **the terminal**: the same overview, plus the repos left out of the CTF ## Requirements - Python 3.9+ - [`requests`](https://pypi.org/project/requests/) and [`PyYAML`](https://pypi.org/project/PyYAML/) (`python-requests python-yaml` on Arch, `python3-requests python3-yaml` on Debian) ## Configuration ```sh cp .env.example .env && chmod 600 .env ``` | Key | Required | Meaning | |---|---|---| | `GITLAB_GROUP_URL` | yes | GitLab group that holds one repo per challenge | | `GITLAB_TOKEN` | yes | Personal access token, see below | | `HEDGEDOC_NOTE` | for `--update` | URL of an existing note; its permission must be **freely** | | `HEDGEDOC_TAGS` | no | Tags written into the note's front matter | | `CHALLS_REPO_URL` | for `--challs` | Deployment repo that holds `challs.txt` | | `CHALLS_PATH` | no | Path of the file in that repo (default `2026/challs.txt`) | | `CHALLS_BRANCH` | no | Branch to commit to (default: the repo's default branch) | The token needs **Project: Read** and **Repository: Read** on the challenges group. For `--challs` it also needs **Repository: Read + Write** on the deployment repo, and permission to push to the branch if it is protected. Environment variables override the values in `.env`. ## Choosing what is in the CTF [`challenges.yml`](challenges.yml) maps each repo path to its state: ```yaml challenges: baby_rsa: included # part of this CTF static_antivirus: abandoned # dropped for good some_chall: postponed # left to be finished for another CTF skip_checks: whoami: [has_solve_script] # OSINT: there is nothing to script ``` - Only **included** challenges appear in the HedgeDoc note and the counts, and only they can end up in `challs.txt`. - **Abandoned** and **postponed** challenges are only listed in the terminal. - A repo that is **not listed** is also only listed in the terminal, in its own section, and is not tracked or deployed until somebody decides. The timers run quietly, so run `./ctf_status.py` by hand to see new repos. - **`skip_checks`** exempts a repo from individual repo checks, named after their functions in [`tracker/checks.py`](tracker/checks.py). Entries for repos that do not exist are flagged under *Needs attention*. If the file is missing or invalid, the run stops rather than committing an empty `challs.txt`. The server reads the file from its checkout, so changes must be pulled there. ## Usage ```sh ./ctf_status.py # print the overview ./ctf_status.py -o overview.md # also write the note's markdown to a file ./ctf_status.py --update # overwrite the HedgeDoc note ./ctf_status.py --challs # commit challs.txt, if it changed ./ctf_status.py -q --challs-out - # preview challs.txt without committing ``` `./ctf_status.py --help` lists all options. With `--quiet`, a run prints nothing unless something changed or failed. ## Deployment On the server, with `.env` in place: ```sh ./install-timer.sh ``` This installs and starts two systemd user timers: | Timer | Runs | Does | |---|---|---| | `ctf-status.timer` | every 30 minutes | `--update`: refresh the HedgeDoc note | | `ctf-challs.timer` | daily at 04:00 | `--challs`: commit `challs.txt` | Re-running the script is safe. To check on the timers: ```sh systemctl --user list-timers 'ctf-*' journalctl --user -u ctf-status.service -n 50 journalctl --user -u ctf-challs.service -n 50 ``` ## How challenges are evaluated Each challenge repo starts from a template whose README begins with: ``` ### Challenge Name - Category: crypto - Difficulty: medium - Event: ... - Author: ... - Tester: ... - Status: Testing Ready ``` The status is one of **Planning → PoC Draft → Testing Ready → Tested → Ready**. Loosely written values are normalised. If a status names several stages, the least advanced one counts. Two kinds of finding are reported: - **Problems** are missing or unclear README fields, such as no category, an unreadable status, or a Testing Ready challenge with no tester. They are listed under *Needs attention* and change nothing else. - **Blockers** are signs that a challenge claiming Testing Ready or beyond is not actually finished: `description.yml` is missing or still the template, its category or flag is wrong, a `deploy_dir` or its `Dockerfile` is missing, or there is no `solve.py`. A blocked challenge (⛔) is kept out of `challs.txt`. `skip_checks` in `challenges.yml` can exempt a challenge from a check. Passing every check does not prove that a challenge builds or solves. That is what testing is for. All rules are in [`tracker/checks.py`](tracker/checks.py), one function each. ## Project layout ``` ctf_status.py command line entry point challenges.yml what is part of this CTF, and check exceptions tracker/ checks.py the rules: README problems and repo blockers challenge.py reads a README header into a Challenge gitlab.py GitLab API access selection.py reads challenges.yml overview.py runs parsing and checks over all repos terminal.py terminal output markdown.py HedgeDoc note challs.py challs.txt hedgedoc.py writes the note to HedgeDoc config.py reads the configuration install-timer.sh installs the systemd units below ctf-*.service/.timer systemd unit templates ```