Skip to content

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:

uv run python cli.py challenge validate challenges/circular/circular.yml

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.

uv run python cli.py challenge import challenges/circular/circular.yml

For a different environment (e.g. production):

uv run python cli.py challenge import challenges/circular/circular.yml \
  --app-env 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:

/admin/challenges/import

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):

uv run python cli.py challenge build ecbeast

Dry-run (shows what would be built without running docker build):

uv run python cli.py challenge build ecbeast --dry-run

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:

uv run python cli.py challenge push ecbeast

Override the registry host:

uv run python cli.py challenge push ecbeast \
  --registry-host registry.ctfhive.us

Dry-run:

uv run python cli.py challenge push ecbeast --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 standardfalse; dynamictrue 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

  1. Create the target category_slug in challenges/manifest.yml if it does not already exist.
  2. Write a YAML file for each challenge following the schema in challenge-format.md.
  3. Place handout files in challenges/<slug>/.
  4. Run uv run python cli.py challenge validate <manifest> for each file.
  5. Fix any validation errors reported.
  6. Run uv run python cli.py challenge import <manifest>.
  7. If the challenge is dynamic, build and push the container image.
  8. For challenges you want visible immediately, set status: visible before 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_type on 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 points value manually.
  • Hints are not implemented in v1. Include essential hints in the challenge description.