Skip to content

Commit

Permalink
Merge pull request #235 from OpenToAllCTF/development
Browse files Browse the repository at this point in the history
Release v1.1.0
  • Loading branch information
Grazfather authored Jan 21, 2021
2 parents b2e29d6 + 59402eb commit 6deea8c
Show file tree
Hide file tree
Showing 7 changed files with 251 additions and 48 deletions.
26 changes: 26 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,32 @@ All notable changes to this project will be kept in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).

## [1.1.0] - 2021-01-20
### Added
* Pulled in features developed for Samurai!
* Ability to create private CTFs (b1e9456)
* Ability to auto-invite all CTF channel members to new challenges(1a087b1)
* Ability to archive only challenges (fff0bda)
* !signup (566b39b)
* !populate (!gather, !summon) (8a7d77d)
* !join (c0f2db9)
* !makectf (cec0d3e)
* !debug (3f7730f)
* get_channel_by_name() (4a36d1d)

### Changed
* Update AddCTFCommand to handle results of call to conversations API. (b1e9456)
* Switch set_purpose from events to conversations API. (a6048e7)
* Implement the conversations API for listing channels. (4a36d1d)
* Update get_channel_members() to use new slack API. (3c8d707)
* Update !archivectf to use new slack API. (c241342)
* Miscellaneous:
* Make category optional when adding challenge (5fc6468)
* Stop set_config_option mangling file indentation (f2394ed)
* Remove player list from status to cut down on noise (c6aebbb)
* Alias !archive and !archivectf (c241342)
* Make !add an alias of !addchallenge (3ff51e7)

## [1.0.0] - 2021-01-18
"Stable" - The development version we've been using for > 6 months. With the
[deprecation of the
Expand Down
6 changes: 5 additions & 1 deletion config/config.json.template
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
"auto_invite" : [],
"wolfram_app_id" : "",
"archive_ctf_reminder_offset" : "168",
"archive_everything": true,
"delete_watch_keywords" : "",
"intro_message" : ""
"intro_message" : "",
"private_ctfs": false,
"allow_signup": false,
"maintenance_mode": false
}
79 changes: 67 additions & 12 deletions handlers/admin_handler.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import json
from bottypes.command import Command
from bottypes.command_descriptor import CommandDesc
from bottypes.invalid_command import InvalidCommand
Expand All @@ -6,6 +7,67 @@
from util.util import (get_display_name_from_user, parse_user_id,
resolve_user_by_user_id)

class MakeCTFCommand():
"""
Update the channel purpose to be that of a CTF.
"""
@classmethod
def execute(cls, slack_wrapper, args, timestamp, channel_id, user_id, user_is_admin):
if user_is_admin:
purpose = {}
purpose["ota_bot"] = "OTABOT"
purpose["name"] = args[0]
purpose["type"] = "CTF"
purpose["cred_user"] = ""
purpose["cred_pw"] = ""
purpose["long_name"] = args[0]
purpose["finished"] = False
purpose["finished_on"] = ""
slack_wrapper.set_purpose(channel_id, purpose)


class StartDebuggerCommand():
"""
Break into pdb. Better have a tty open!
Must be in maintenance mode to use.
"""

@classmethod
def execute(cls, slack_wrapper, args, timestamp, channel_id, user_id, user_is_admin):
if user_is_admin:
if handler_factory.botserver.get_config_option("maintenance_mode"):
import pdb; pdb.set_trace()
else:
InvalidCommand("Must be in maintenance mode to open a shell")


class JoinChannelCommand():
"""
Join the named channel.
"""

@classmethod
def execute(cls, slack_wrapper, args, timestamp, channel_id, user_id, user_is_admin):
if user_is_admin:
channel = slack_wrapper.get_channel_by_name(args[0])
if channel:
slack_wrapper.invite_user(user_id, channel['id'])
else:
slack_wrapper.post_message(user_id, "No such channel")


class ToggleMaintenanceModeCommand(Command):
"""Update maintenance mode configuration."""

@classmethod
def execute(cls, slack_wrapper, args, timestamp, channel_id, user_id, user_is_admin):
"""Execute the ToggleMaintenanceModeCommand command."""
mode = not bool(handler_factory.botserver.get_config_option("maintenance_mode"))
state = "enabled" if mode else "disabled"
handler_factory.botserver.set_config_option("maintenance_mode", mode)
text = "Maintenance mode " + state
slack_wrapper.post_message(channel_id, text)


class ShowAdminsCommand(Command):
"""Shows list of users in the admin user group."""
Expand Down Expand Up @@ -113,25 +175,18 @@ def execute(cls, slack_wrapper, args, timestamp, channel_id, user_id, user_is_ad
class AdminHandler(BaseHandler):
"""
Handles configuration options for administrators.
Commands :
# Show administrator users
!admin show_admins
# Add a user to the administrator group
!admin add_admin user_id
# Remove a user from the administrator group
!admin remove_admin user_id
"""

def __init__(self):
self.commands = {
"show_admins": CommandDesc(ShowAdminsCommand, "Show a list of current admin users", None, None, True),
"add_admin": CommandDesc(AddAdminCommand, "Add a user to the admin user group", ["user_id"], None, True),
"remove_admin": CommandDesc(RemoveAdminCommand, "Remove a user from the admin user group", ["user_id"], None, True),
"as": CommandDesc(AsCommand, "Execute a command as another user", ["@user", "command"], None, True)
"as": CommandDesc(AsCommand, "Execute a command as another user", ["@user", "command"], None, True),
"maintenance": CommandDesc(ToggleMaintenanceModeCommand, "Toggle maintenance mode", None, None, True),
"debug": CommandDesc(StartDebuggerCommand, "Break into a debugger shell", None, None, True),
"join": CommandDesc(JoinChannelCommand, "Join a channel", ["channel_name"], None, True),
"makectf": CommandDesc(MakeCTFCommand, "Turn the current channel into a CTF channel by setting the purpose. Requires reload to take effect", ["ctf_name"], None, True)
}


handler_factory.register("admin", AdminHandler())
11 changes: 11 additions & 0 deletions handlers/base_handler.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from abc import ABC

from bottypes.invalid_command import InvalidCommand
from handlers import handler_factory


class BaseHandler(ABC):
Expand Down Expand Up @@ -86,6 +87,13 @@ def get_usage(self, user_is_admin):
return msg

def process(self, slack_wrapper, command, args, timestamp, channel, user, user_is_admin):
if handler_factory.botserver.get_config_option("maintenance_mode"):
if user_is_admin:
slack_wrapper.post_message(channel, "Warning! Maintenance mode is enabled!", timestamp)
else:
slack_wrapper.post_message(channel, "Down for maintenance, back soon.", timestamp)
return

"""Check if enough arguments were passed for this command."""
if command in self.aliases:
self.process(slack_wrapper, self.aliases[command], args, timestamp, channel, user, user_is_admin)
Expand All @@ -98,6 +106,9 @@ def process(self, slack_wrapper, command, args, timestamp, channel, user, user_i
cmd_descriptor.command.execute(slack_wrapper, args, timestamp, channel, user, user_is_admin)

def process_reaction(self, slack_wrapper, reaction, channel, timestamp, user, user_is_admin):
if handler_factory.botserver.get_config_option("maintenance_mode") and not user_is_admin:
raise InvalidCommand("Down for maintenance, back soon.")

reaction_descriptor = self.reactions[reaction]

if reaction_descriptor:
Expand Down
112 changes: 94 additions & 18 deletions handlers/challenge_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,60 @@
from util.solveposthelper import ST_GIT_SUPPORT, post_ctf_data
from util.util import *

class SignupCommand():
"""
Invite the user into the specified CTF channel along with any existing challenge channels.
"""

@classmethod
def execute(cls, slack_wrapper, args, timestamp, channel_id, user_id, user_is_admin):
enabled = handler_factory.botserver.get_config_option("allow_signup")
ctf = get_ctf_by_name(ChallengeHandler.DB, args[0])
if not enabled or not ctf:
raise InvalidCommand("No CTF by that name")

if ctf.finished:
raise InvalidCommand("That CTF has already concluded")

members = [user_id]
current = slack_wrapper.get_channel_members(ctf.channel_id)
invites = list(set(members)-set(current))

# Ignore responses, because errors here don't matter
if len(invites) > 0:
response = slack_wrapper.invite_user(invites, ctf.channel_id)
for chall in get_challenges_for_ctf_id(ChallengeHandler.DB, ctf.channel_id):
current = slack_wrapper.get_channel_members(chall.channel_id)
invites = list(set(members)-set(current))
if len(invites) > 0:
response = slack_wrapper.invite_user(invites, chall.channel_id)


class PopulateCommand():
"""
Invite a list of members to the CTF channel and add them to any existing
challenge channels.
"""

@classmethod
def execute(cls, slack_wrapper, args, timestamp, channel_id, user_id, user_is_admin):
ctf = get_ctf_by_channel_id(ChallengeHandler.DB, channel_id)
if not ctf:
raise InvalidCommand("You must be in a CTF or Challenge channel to use this command.")

members = [user.strip("<>@") for user in args]
current = slack_wrapper.get_channel_members(ctf.channel_id)
invites = list(set(members)-set(current))

# Ignore responses, because errors here don't matter
if len(invites) > 0:
slack_wrapper.invite_user(invites, ctf.channel_id)
for chall in get_challenges_for_ctf_id(ChallengeHandler.DB, ctf.channel_id):
current = slack_wrapper.get_channel_members(chall.channel_id)
invites = list(set(members)-set(current))
if len(invites) > 0:
slack_wrapper.invite_user(invites, chall.channel_id)


class AddChallengeTagCommand(Command):
"""Add a tag or tags to a challenge"""
Expand Down Expand Up @@ -115,7 +169,8 @@ def execute(cls, slack_wrapper, args, timestamp, channel_id, user_id, user_is_ad
raise InvalidCommand("Add CTF failed: Invalid characters for CTF name found.")

# Create the channel
response = slack_wrapper.create_channel(name)
private_ctf = handler_factory.botserver.get_config_option("private_ctfs")
response = slack_wrapper.create_channel(name, is_private=private_ctf)

# Validate that the channel was successfully created.
if not response['ok']:
Expand Down Expand Up @@ -290,7 +345,7 @@ def execute(cls, slack_wrapper, args, timestamp, channel_id, user_id, user_is_ad
raise InvalidCommand("\"{}\" channel creation failed:\nError : {}".format(channel_name, response['error']))

# Add purpose tag for persistence
challenge_channel_id = response['group']['id']
challenge_channel_id = response['channel']['id']
purpose = dict(ChallengeHandler.CHALL_PURPOSE)
purpose['name'] = name
purpose['ctf_id'] = ctf.channel_id
Expand All @@ -299,9 +354,16 @@ def execute(cls, slack_wrapper, args, timestamp, channel_id, user_id, user_is_ad
purpose = json.dumps(purpose)
slack_wrapper.set_purpose(challenge_channel_id, purpose, is_private=True)

# Invite everyone in the auto-invite list
for invite_user_id in handler_factory.botserver.get_config_option("auto_invite"):
slack_wrapper.invite_user(invite_user_id, challenge_channel_id, is_private=True)
if handler_factory.botserver.get_config_option("auto_invite") == True:
# Invite everyone in the ctf channel
members = slack_wrapper.get_channel_members(ctf.channel_id)
present = slack_wrapper.get_channel_members(challenge_channel_id)
invites = list(set(members)-set(present))
slack_wrapper.invite_user(invites, challenge_channel_id)
else:
# Invite everyone in the auto-invite list
for invite_user_id in handler_factory.botserver.get_config_option("auto_invite"):
slack_wrapper.invite_user(invite_user_id, challenge_channel_id, is_private=True)

# New Challenge
challenge = Challenge(ctf.channel_id, challenge_channel_id, name, category)
Expand Down Expand Up @@ -513,12 +575,11 @@ def build_verbose_status(cls, slack_wrapper, ctf_list, check_for_finish, categor
if player_id in members:
players.append(members[player_id])

response += "[{} active] *{}* {}: {} {}\n". format(
response += "[{} active] *{}* {}: {}\n". format(
len(players),
challenge.name,
"[{}]".format(", ".join(challenge.tags)) if len(challenge.tags) > 0 else "",
"({})".format(challenge.category) if challenge.category else "",
transliterate(", ".join(players)))
"({})".format(challenge.category) if challenge.category else "")
response = response.strip()

if response == "": # Response is empty
Expand Down Expand Up @@ -787,7 +848,7 @@ def execute(cls, slack_wrapper, args, timestamp, channel_id, user_id, user_is_ad
message = "Archived the following channels :\n"
for challenge in challenges:
message += "- #{}-{}\n".format(ctf.name, challenge.name)
slack_wrapper.archive_private_channel(challenge.channel_id)
slack_wrapper.archive_channel(challenge.channel_id)
remove_challenge_by_channel_id(ChallengeHandler.DB, challenge.channel_id, ctf.channel_id)

# Remove possible configured reminders for this ctf
Expand All @@ -800,8 +861,16 @@ def execute(cls, slack_wrapper, args, timestamp, channel_id, user_id, user_is_ad
# Show confirmation message
slack_wrapper.post_message(channel_id, message)

# Archive the main CTF channel also to cleanup
slack_wrapper.archive_public_channel(channel_id)
# If configured to do so, archive the main CTF channel also to cleanup
if handler_factory.botserver.get_config_option('archive_everything'):
slack_wrapper.archive_channel(channel_id)
else:
# Otherwise, just set the ctf to finished
if not ctf.finished:
ctf.finished = True
ctf.finished_on = int(time.time())




class EndCTFCommand(Command):
Expand Down Expand Up @@ -943,7 +1012,8 @@ class ChallengeHandler(BaseHandler):
"cred_user": "",
"cred_pw": "",
"long_name": "",
"finished": False
"finished": False,
"finished_on": 0
}

CHALL_PURPOSE = {
Expand All @@ -958,9 +1028,10 @@ class ChallengeHandler(BaseHandler):
def __init__(self):
self.commands = {
"addctf": CommandDesc(AddCTFCommand, "Adds a new ctf", ["ctf_name", "long_name"], None),
"addchallenge": CommandDesc(AddChallengeCommand, "Adds a new challenge for current ctf", ["challenge_name", "challenge_category"], None),
"addchallenge": CommandDesc(AddChallengeCommand, "Adds a new challenge for current ctf", ["challenge_name"], ["challenge_category"]),
"workon": CommandDesc(WorkonCommand, "Show that you're working on a challenge", None, ["challenge_name"]),
"status": CommandDesc(StatusCommand, "Show the status for all ongoing ctf's", None, ["category"]),
"signup": CommandDesc(SignupCommand, "Join a CTF", None, ["ctf_name"], None),
"solve": CommandDesc(SolveCommand, "Mark a challenge as solved", None, ["challenge_name", "support_member"]),
"renamechallenge": CommandDesc(RenameChallengeCommand, "Renames a challenge", ["old_challenge_name", "new_challenge_name"], None),
"renamectf": CommandDesc(RenameCTFCommand, "Renames a ctf", ["old_ctf_name", "new_ctf_name"], None),
Expand All @@ -973,6 +1044,7 @@ def __init__(self):
"unsolve": CommandDesc(UnsolveCommand, "Remove solve of a challenge", None, ["challenge_name"]),
"removechallenge": CommandDesc(RemoveChallengeCommand, "Remove challenge", None, ["challenge_name"], True),
"removetag": CommandDesc(RemoveChallengeTagCommand, "Remove tag(s) from a challenge", ["challenge_tag/name"], ["[..challenge_tag(s)]"]),
"populate": CommandDesc(PopulateCommand, "Invite all non-present members of the CTF challenge into the challenge channel", None, None),
"roll": CommandDesc(RollCommand, "Roll the dice", None, None)
}
self.reactions = {
Expand All @@ -982,6 +1054,10 @@ def __init__(self):
self.aliases = {
"finishctf": "endctf",
"addchall": "addchallenge",
"add": "addchallenge",
"archive": "archivectf",
"gather": "populate",
"summon": "populate"
}

@staticmethod
Expand All @@ -1007,8 +1083,8 @@ def update_database_from_slack(slack_wrapper):
Reload the ctf and challenge information from slack.
"""
database = {}
privchans = slack_wrapper.get_private_channels()["groups"]
pubchans = slack_wrapper.get_public_channels()["channels"]
privchans = slack_wrapper.get_private_channels()
pubchans = slack_wrapper.get_public_channels()

# Find active CTF channels
for channel in [*privchans, *pubchans]:
Expand All @@ -1028,8 +1104,7 @@ def update_database_from_slack(slack_wrapper):
pass

# Find active challenge channels
response = slack_wrapper.get_private_channels()
for channel in response['groups']:
for channel in privchans:
try:
purpose = load_json(channel['purpose']['value'])

Expand All @@ -1044,7 +1119,8 @@ def update_database_from_slack(slack_wrapper):
challenge.mark_as_solved(solvers, purpose.get("solve_date"))

if ctf:
for member_id in channel['members']:
members = slack_wrapper.get_channel_members(channel['id'])
for member_id in members:
if member_id != slack_wrapper.user_id:
challenge.add_player(Player(member_id))

Expand Down
Loading

0 comments on commit 6deea8c

Please sign in to comment.