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 and PyYAML (python-requests python-yaml on Arch, python3-requests python3-yaml on Debian)

Configuration

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 maps each repo path to its state:

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.

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

./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:

./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:

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, 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
S
Description
No description provided
Readme
167 KiB
Languages
Python 97%
Shell 3%