Skip to content

Commit

Permalink
Add Challenge.clone staticmethod to clone challenges from remote
Browse files Browse the repository at this point in the history
  • Loading branch information
ColdHeat committed Aug 20, 2024
1 parent 40f72fa commit 2ea8358
Show file tree
Hide file tree
Showing 4 changed files with 63 additions and 9 deletions.
22 changes: 14 additions & 8 deletions ctfcli/cli/challenges.py
Original file line number Diff line number Diff line change
Expand Up @@ -928,11 +928,13 @@ def mirror(
files_directory: str = "dist",
skip_verify: bool = False,
ignore: Union[str, Tuple[str]] = (),
create: bool = False,
) -> int:
log.debug(
f"mirror: (challenge={challenge}, files_directory={files_directory}, "
f"skip_verify={skip_verify}, ignore={ignore})"
)
config = Config()

if challenge:
challenge_instance = self._resolve_single_challenge(challenge)
Expand All @@ -947,18 +949,22 @@ def mirror(
ignore = (ignore,)

remote_challenges = Challenge.load_installed_challenges()
if len(local_challenges) > 1:
# Issue a warning if there are extra challenges on the remote that do not have a local version
local_challenge_names = [c["name"] for c in local_challenges]

for remote_challenge in remote_challenges:
if remote_challenge["name"] not in local_challenge_names:
# Issue a warning if there are extra challenges on the remote that do not have a local version
local_challenge_names = [c["name"] for c in local_challenges]
for remote_challenge in remote_challenges:
if remote_challenge["name"] not in local_challenge_names:
click.secho(
f"Found challenge '{remote_challenge['name']}' in CTFd, but not in .ctf/config",
fg="yellow",
)
if create:
click.secho(
f"Found challenge '{remote_challenge['name']}' in CTFd, but not in .ctf/config\n"
"Mirroring does not create new local challenges\n"
"Please add the local challenge if you wish to manage it with ctfcli\n",
f"Mirroring '{remote_challenge['name']}' to local due to --create",
fg="yellow",
)
challenge_instance = Challenge.clone(config=config, remote_challenge=remote_challenge)
challenge_instance.mirror(files_directory_name=files_directory, ignore=ignore)

failed_mirrors = []
with click.progressbar(local_challenges, label="Mirroring challenges") as challenges:
Expand Down
45 changes: 44 additions & 1 deletion ctfcli/core/challenge.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import logging
import re
import subprocess
from os import PathLike
Expand All @@ -6,10 +7,12 @@

import click
import yaml
from cookiecutter.main import cookiecutter
from slugify import slugify

from ctfcli.core.api import API
from ctfcli.core.exceptions import (
ChallengeException,
InvalidChallengeDefinition,
InvalidChallengeFile,
LintException,
Expand All @@ -19,6 +22,8 @@
from ctfcli.utils.hashing import hash_file
from ctfcli.utils.tools import strings

log = logging.getLogger("ctfcli.core.challenge")


def str_presenter(dumper, data):
if len(data.splitlines()) > 1 or "\n" in data:
Expand Down Expand Up @@ -100,6 +105,43 @@ def is_default_challenge_property(key: str, value: Any) -> bool:

return False

@staticmethod
def clone(config, remote_challenge):
name = remote_challenge["name"]

if name is None:
raise ChallengeException(f'Could not get name of remote challenge with id {remote_challenge["id"]}')

# First, generate a name for the challenge directory
category = remote_challenge.get("category", None)
challenge_dir_name = slugify(name)
if category is not None:
challenge_dir_name = str(Path(slugify(category)) / challenge_dir_name)

if Path(challenge_dir_name).exists():
raise ChallengeException(
f"Challenge directory '{challenge_dir_name}' for challenge '{name}' already exists"
)

# Create an blank/empty challenge, with only the challenge.yml containing the challenge name
template_path = config.get_base_path() / "templates" / "blank" / "empty"
log.debug(f"Challenge.clone: cookiecutter({str(template_path)}, {name=}, {challenge_dir_name=}")
cookiecutter(
str(template_path),
no_input=True,
extra_context={"name": name, "dirname": challenge_dir_name},
)

if not Path(challenge_dir_name).exists():
raise ChallengeException(f"Could not create challenge directory '{challenge_dir_name}' for '{name}'")

# Add the newly created local challenge to the config file
config["challenges"][challenge_dir_name] = challenge_dir_name
with open(config.config_path, "w+") as f:
config.write(f)

return Challenge(f"{challenge_dir_name}/challenge.yml")

@property
def api(self):
if not self._api:
Expand All @@ -110,6 +152,7 @@ def api(self):
# __init__ expects an absolute path to challenge_yml, or a relative one from the cwd
# it does not join that path with the project_path
def __init__(self, challenge_yml: Union[str, PathLike], overrides=None):
log.debug(f"Challenge.__init__: ({challenge_yml=}, {overrides=}")
if overrides is None:
overrides = {}

Expand Down Expand Up @@ -209,7 +252,7 @@ def _load_challenge_id(self):

def _validate_files(self):
# if the challenge defines files, make sure they exist before making any changes to the challenge
for challenge_file in self["files"]:
for challenge_file in self.get("files", []):
if not (self.challenge_directory / challenge_file).exists():
raise InvalidChallengeFile(f"File {challenge_file} could not be loaded")

Expand Down
4 changes: 4 additions & 0 deletions ctfcli/templates/blank/empty/cookiecutter.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "challenge",
"dirname": "challenge"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
name: "{{cookiecutter.name}}"

0 comments on commit 2ea8358

Please sign in to comment.