Importing challenges¶
There are three ways to get challenges into a running event: the ops CLI, the admin panel, and the build/push workflow for container images.
1. CLI — validate then import¶
The cli.py script (run with uv run python cli.py) provides the canonical
workflow for importing challenges from the command line.
Validate first¶
Validate one or more manifests against the schema without touching the database:
With multiple manifests:
uv run python cli.py challenge validate \
challenges/circular/circular.yml \
challenges/ECBeast/ecbeast-challenge.yml
Successful output:
{
"status": "ok",
"results": [
{
"manifest": "challenges/circular/circular.yml",
"slug": "circular",
"title": "Circular",
"dynamic": false,
"files": ["chal.py", "output.txt"],
"has_lab_stack": false
}
]
}
Add --json to always get machine-readable JSON output.
Import¶
Once validation passes, run the import. The importer upserts the Challenge
row (creates on first run, updates on re-run) and syncs ChallengeFile rows.
If status: visible, it also pre-generates flags for all existing principals.
For a different environment (e.g. production):
Import multiple challenges at once:
uv run python cli.py challenge import \
challenges/circular/circular.yml \
challenges/partial-smith/partial-smith.yml \
challenges/ECBeast/ecbeast-challenge.yml
Successful output:
{
"status": "ok",
"results": [
{
"status": "ok",
"action": "created",
"slug": "circular",
"challenge_id": 1,
"has_lab_stack": false,
"workspace_status": "pending"
}
]
}
action is "created" on first import and "updated" on re-import.
2. Admin panel — YAML upload¶
The admin panel accepts one or more YAML files at:
Navigate there, click Choose files, select one or more .yml or .yaml
files, and submit. The UI shows a results table — one row per file — with
ok, skipped (non-YAML extension), or error (validation failure with
reason).
Only .yml and .yaml files are accepted; other extensions are skipped
silently.
The admin YAML import calls the same import_challenge_yaml function as the
CLI. The behaviour is identical: upsert on re-upload, flag pre-generation when
status: visible.
3. Building and pushing container images¶
For challenges with is_dynamic: true, the container image must be built and
available to the Docker daemon before players can spawn instances.
Build¶
Build the Docker image for a challenge by slug (or numeric ID):
Dry-run (shows what would be built without running docker build):
The build command looks for a Dockerfile in challenges/<slug>/ or in the
directory that contains the image ref. If no local source is found, the image
is expected to already exist in the daemon or registry.
Push¶
Push the built image to the configured registry:
Override the registry host:
Dry-run:
Registry check
Run uv run python cli.py registry check to see all challenge images the
platform knows about and whether a local Docker source exists for each.
Importing from CTFd¶
No automated CTFd importer exists
There is no import script, API bridge, or one-click migration tool that reads a CTFd challenge export and produces CTFhive YAML. Migration is a manual field-mapping exercise.
The fundamental reason is that CTFd uses static flag strings, while CTFhive derives a unique flag per team using HMAC-SHA3-256. A static flag from a CTFd export cannot be used directly — the flag model is incompatible by design.
Field mapping table¶
Use the table below when translating a CTFd challenge export (typically a
directory of files plus a challenges.json) into CTFhive YAML:
| CTFd field | CTFhive field | Notes |
|---|---|---|
name |
title |
Direct copy. |
category |
category_slug |
Convert to lowercase, replace spaces with -. Must match a key in challenges/manifest.yml. |
value |
points |
Direct copy. |
description |
description_md |
Copy as-is; it is already Markdown in most CTFd exports. |
type (standard, dynamic) |
is_dynamic |
standard → false; dynamic → true if you have a container image. |
| Static flag string(s) | not applicable | CTFhive flags are always derived per-team. There is no way to import a CTFd static flag value. Instead, set the correct flag_prefix, import the challenge, and distribute per-team flags from the admin dashboard. |
| Dynamic scoring (CTFd) | not implemented | CTFhive awards points as a fixed value on solve. Decay curves are not yet implemented. See scoreboard-scoring.md for the current model. |
files |
files |
Copy attachment paths into the files list. Re-upload the actual files via the admin panel or place them in the workspace directory. |
hints |
not implemented | Hint infrastructure does not exist in v1. Add hint text to description_md or omit. |
tags |
not applicable | CTFhive has no tag system; omit. |
connection_info |
container_image + container_port |
If the challenge had a spawnable container, fill these fields. If not, omit. |
Migration checklist¶
- Create the target
category_sluginchallenges/manifest.ymlif it does not already exist. - Write a YAML file for each challenge following the schema in challenge-format.md.
- Place handout files in
challenges/<slug>/. - Run
uv run python cli.py challenge validate <manifest>for each file. - Fix any validation errors reported.
- Run
uv run python cli.py challenge import <manifest>. - If the challenge is dynamic, build and push the container image.
- For challenges you want visible immediately, set
status: visiblebefore importing. Players who have already registered will have flags pre-generated automatically.
Caveats¶
- Per-team flag derivation is not optional. Every challenge in CTFhive
uses HMAC-derived flags, even if
flag_typeon the model says"derived". There is no way to configure a static flag string through the YAML importer. - Dynamic scoring is not implemented. If a CTFd event used dynamic
scoring (decay curves), you must choose a fixed
pointsvalue manually. - Hints are not implemented in v1. Include essential hints in the challenge description.