Skip to content

Commit fcaf51a

Browse files
authored
Add healthcheck command and configuration (#77)
* Add `healthcheck` key in `challenge.yml` to specify a healthcheck script * Add `ctf challenge healthcheck [challenge_name]` * Closes #75
1 parent 3892b71 commit fcaf51a

File tree

3 files changed

+65
-0
lines changed

3 files changed

+65
-0
lines changed

ctfcli/cli/challenges.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import os
22
import subprocess
3+
import sys
34
from pathlib import Path
45
from urllib.parse import urlparse
56

@@ -11,6 +12,7 @@
1112
create_challenge,
1213
lint_challenge,
1314
load_challenge,
15+
load_installed_challenge,
1416
load_installed_challenges,
1517
sync_challenge,
1618
)
@@ -353,3 +355,56 @@ def push(self, challenge=None):
353355
click.echo(
354356
"Couldn't process that challenge path. Please check that the challenge is added to .ctf/config and that your path matches."
355357
)
358+
359+
def healthcheck(self, challenge):
360+
config = load_config()
361+
challenges = config["challenges"]
362+
363+
# challenge_path = challenges[challenge]
364+
path = Path(challenge)
365+
if path.name.endswith(".yml") is False:
366+
path = path / "challenge.yml"
367+
368+
challenge = load_challenge(path)
369+
click.secho(f'Loaded {challenge["name"]}', fg="yellow")
370+
try:
371+
healthcheck = challenge["healthcheck"]
372+
except KeyError:
373+
click.secho(f'{challenge["name"]} missing healthcheck parameter', fg="red")
374+
return
375+
376+
# Get challenges installed from CTFd and try to find our challenge
377+
installed_challenges = load_installed_challenges()
378+
target = None
379+
for c in installed_challenges:
380+
if c["name"] == challenge["name"]:
381+
target = c
382+
break
383+
else:
384+
click.secho(
385+
f'Couldn\'t find challenge {c["name"]} on CTFd', fg="red",
386+
)
387+
return
388+
389+
# Get the actual challenge data
390+
installed_challenge = load_installed_challenge(target["id"])
391+
connection_info = installed_challenge["connection_info"]
392+
393+
# Run healthcheck
394+
if connection_info:
395+
rcode = subprocess.call(
396+
[healthcheck, "--connection-info", connection_info], cwd=path.parent
397+
)
398+
else:
399+
rcode = subprocess.call([healthcheck], cwd=path.parent)
400+
401+
if rcode != 0:
402+
click.secho(
403+
f"Healcheck failed", fg="red",
404+
)
405+
sys.exit(1)
406+
else:
407+
click.secho(
408+
f"Success", fg="green",
409+
)
410+
sys.exit(0)

ctfcli/spec/challenge-example.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@ host: null
3636
# connection_info is used to provide a link, hostname, or instructions on how to connect to a challenge
3737
connection_info: nc hostname 12345
3838

39+
# healthcheck is a tool/script used to check a challenge
40+
# If connection_info was provided to CTFd when the challenge was installed, it will be passed to the healthcheck script:
41+
# ./writeup/exploit.sh --connection-info "nc hostname 12345"
42+
healthcheck: writeup/exploit.sh
43+
3944
# Can be removed if unused
4045
attempts: 5
4146

ctfcli/utils/challenge.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ def load_challenge(path):
2323
return
2424

2525

26+
def load_installed_challenge(challenge_id):
27+
s = generate_session()
28+
return s.get(f"/api/v1/challenges/{challenge_id}", json=True).json()["data"]
29+
30+
2631
def load_installed_challenges():
2732
s = generate_session()
2833
return s.get("/api/v1/challenges?view=admin", json=True).json()["data"]

0 commit comments

Comments
 (0)