Merge pull request #259 from guardicore/feature/run-aws-monkey
Feature/run aws monkey
This commit is contained in:
commit
0513966c76
|
@ -4,7 +4,7 @@ import urllib2
|
|||
__author__ = 'itay.mizeretz'
|
||||
|
||||
|
||||
class AWS(object):
|
||||
class AwsInstance(object):
|
||||
def __init__(self):
|
||||
try:
|
||||
self.instance_id = urllib2.urlopen('http://169.254.169.254/latest/meta-data/instance-id', timeout=2).read()
|
|
@ -0,0 +1,63 @@
|
|||
import boto3
|
||||
from botocore.exceptions import ClientError
|
||||
|
||||
__author__ = 'itay.mizeretz'
|
||||
|
||||
|
||||
class AwsService(object):
|
||||
"""
|
||||
Supplies various AWS services
|
||||
"""
|
||||
|
||||
access_key_id = None
|
||||
secret_access_key = None
|
||||
region = None
|
||||
|
||||
@staticmethod
|
||||
def set_auth_params(access_key_id, secret_access_key):
|
||||
AwsService.access_key_id = access_key_id
|
||||
AwsService.secret_access_key = secret_access_key
|
||||
|
||||
@staticmethod
|
||||
def set_region(region):
|
||||
AwsService.region = region
|
||||
|
||||
@staticmethod
|
||||
def get_client(client_type, region=None):
|
||||
return boto3.client(
|
||||
client_type,
|
||||
aws_access_key_id=AwsService.access_key_id,
|
||||
aws_secret_access_key=AwsService.secret_access_key,
|
||||
region_name=region if region is not None else AwsService.region)
|
||||
|
||||
@staticmethod
|
||||
def get_session():
|
||||
return boto3.session.Session(
|
||||
aws_access_key_id=AwsService.access_key_id,
|
||||
aws_secret_access_key=AwsService.secret_access_key)
|
||||
|
||||
@staticmethod
|
||||
def get_regions():
|
||||
return AwsService.get_session().get_available_regions('ssm')
|
||||
|
||||
@staticmethod
|
||||
def test_client():
|
||||
try:
|
||||
AwsService.get_client('ssm').describe_instance_information()
|
||||
return True
|
||||
except ClientError:
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def get_instances():
|
||||
return \
|
||||
[
|
||||
{
|
||||
'instance_id': x['InstanceId'],
|
||||
'name': x['ComputerName'],
|
||||
'os': x['PlatformType'].lower(),
|
||||
'ip_address': x['IPAddress']
|
||||
}
|
||||
for x in AwsService.get_client('ssm').describe_instance_information()['InstanceInformationList']
|
||||
]
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
from common.cmd.cmd_result import CmdResult
|
||||
|
||||
|
||||
__author__ = 'itay.mizeretz'
|
||||
|
||||
|
||||
class AwsCmdResult(CmdResult):
|
||||
"""
|
||||
Class representing an AWS command result
|
||||
"""
|
||||
|
||||
def __init__(self, command_info):
|
||||
super(AwsCmdResult, self).__init__(
|
||||
self.is_successful(command_info, True), command_info[u'ResponseCode'], command_info[u'StandardOutputContent'],
|
||||
command_info[u'StandardErrorContent'])
|
||||
self.command_info = command_info
|
||||
|
||||
@staticmethod
|
||||
def is_successful(command_info, is_timeout=False):
|
||||
"""
|
||||
Determines whether the command was successful. If it timed out and was still in progress, we assume it worked.
|
||||
:param command_info: Command info struct (returned by ssm.get_command_invocation)
|
||||
:param is_timeout: Whether the given command timed out
|
||||
:return: True if successful, False otherwise.
|
||||
"""
|
||||
return (command_info[u'Status'] == u'Success') or (is_timeout and (command_info[u'Status'] == u'InProgress'))
|
|
@ -0,0 +1,42 @@
|
|||
import logging
|
||||
|
||||
from common.cloud.aws_service import AwsService
|
||||
from common.cmd.aws.aws_cmd_result import AwsCmdResult
|
||||
from common.cmd.cmd_runner import CmdRunner
|
||||
from common.cmd.cmd_status import CmdStatus
|
||||
|
||||
__author__ = 'itay.mizeretz'
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AwsCmdRunner(CmdRunner):
|
||||
"""
|
||||
Class for running commands on a remote AWS machine
|
||||
"""
|
||||
|
||||
def __init__(self, is_linux, instance_id, region = None):
|
||||
super(AwsCmdRunner, self).__init__(is_linux)
|
||||
self.instance_id = instance_id
|
||||
self.region = region
|
||||
self.ssm = AwsService.get_client('ssm', region)
|
||||
|
||||
def query_command(self, command_id):
|
||||
return self.ssm.get_command_invocation(CommandId=command_id, InstanceId=self.instance_id)
|
||||
|
||||
def get_command_result(self, command_info):
|
||||
return AwsCmdResult(command_info)
|
||||
|
||||
def get_command_status(self, command_info):
|
||||
if command_info[u'Status'] == u'InProgress':
|
||||
return CmdStatus.IN_PROGRESS
|
||||
elif command_info[u'Status'] == u'Success':
|
||||
return CmdStatus.SUCCESS
|
||||
else:
|
||||
return CmdStatus.FAILURE
|
||||
|
||||
def run_command_async(self, command_line):
|
||||
doc_name = "AWS-RunShellScript" if self.is_linux else "AWS-RunPowerShellScript"
|
||||
command_res = self.ssm.send_command(DocumentName=doc_name, Parameters={'commands': [command_line]},
|
||||
InstanceIds=[self.instance_id])
|
||||
return command_res['Command']['CommandId']
|
|
@ -0,0 +1,11 @@
|
|||
__author__ = 'itay.mizeretz'
|
||||
|
||||
|
||||
class Cmd(object):
|
||||
"""
|
||||
Class representing a command
|
||||
"""
|
||||
|
||||
def __init__(self, cmd_runner, cmd_id):
|
||||
self.cmd_runner = cmd_runner
|
||||
self.cmd_id = cmd_id
|
|
@ -0,0 +1,13 @@
|
|||
__author__ = 'itay.mizeretz'
|
||||
|
||||
|
||||
class CmdResult(object):
|
||||
"""
|
||||
Class representing a command result
|
||||
"""
|
||||
|
||||
def __init__(self, is_success, status_code=None, stdout=None, stderr=None):
|
||||
self.is_success = is_success
|
||||
self.status_code = status_code
|
||||
self.stdout = stdout
|
||||
self.stderr = stderr
|
|
@ -0,0 +1,158 @@
|
|||
import time
|
||||
import logging
|
||||
from abc import abstractmethod
|
||||
|
||||
from common.cmd.cmd import Cmd
|
||||
from common.cmd.cmd_result import CmdResult
|
||||
from common.cmd.cmd_status import CmdStatus
|
||||
|
||||
__author__ = 'itay.mizeretz'
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CmdRunner(object):
|
||||
"""
|
||||
Interface for running commands on a remote machine
|
||||
|
||||
Since these classes are a bit complex, I provide a list of common terminology and formats:
|
||||
* command line - a command line. e.g. 'echo hello'
|
||||
* command - represent a single command which was already run. Always of type Cmd
|
||||
* command id - any unique identifier of a command which was already run
|
||||
* command result - represents the result of running a command. Always of type CmdResult
|
||||
* command status - represents the current status of a command. Always of type CmdStatus
|
||||
* command info - Any consistent structure representing additional information of a command which was already run
|
||||
* instance - a machine that commands will be run on. Can be any dictionary with 'instance_id' as a field
|
||||
* instance_id - any unique identifier of an instance (machine). Can be of any format
|
||||
"""
|
||||
|
||||
# Default command timeout in seconds
|
||||
DEFAULT_TIMEOUT = 5
|
||||
# Time to sleep when waiting on commands.
|
||||
WAIT_SLEEP_TIME = 1
|
||||
|
||||
def __init__(self, is_linux):
|
||||
self.is_linux = is_linux
|
||||
|
||||
def run_command(self, command_line, timeout=DEFAULT_TIMEOUT):
|
||||
"""
|
||||
Runs the given command on the remote machine
|
||||
:param command_line: The command line to run
|
||||
:param timeout: Timeout in seconds for command.
|
||||
:return: Command result
|
||||
"""
|
||||
c_id = self.run_command_async(command_line)
|
||||
return self.wait_commands([Cmd(self, c_id)], timeout)[1]
|
||||
|
||||
@staticmethod
|
||||
def run_multiple_commands(instances, inst_to_cmd, inst_n_cmd_res_to_res):
|
||||
"""
|
||||
Run multiple commands on various instances
|
||||
:param instances: List of instances.
|
||||
:param inst_to_cmd: Function which receives an instance, runs a command asynchronously and returns Cmd
|
||||
:param inst_n_cmd_res_to_res: Function which receives an instance and CmdResult
|
||||
and returns a parsed result (of any format)
|
||||
:return: Dictionary with 'instance_id' as key and parsed result as value
|
||||
"""
|
||||
command_instance_dict = {}
|
||||
|
||||
for instance in instances:
|
||||
command = inst_to_cmd(instance)
|
||||
command_instance_dict[command] = instance
|
||||
|
||||
instance_results = {}
|
||||
command_result_pairs = CmdRunner.wait_commands(command_instance_dict.keys())
|
||||
for command, result in command_result_pairs:
|
||||
instance = command_instance_dict[command]
|
||||
instance_results[instance['instance_id']] = inst_n_cmd_res_to_res(instance, result)
|
||||
|
||||
return instance_results
|
||||
|
||||
@abstractmethod
|
||||
def run_command_async(self, command_line):
|
||||
"""
|
||||
Runs the given command on the remote machine asynchronously.
|
||||
:param command_line: The command line to run
|
||||
:return: Command ID (in any format)
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@staticmethod
|
||||
def wait_commands(commands, timeout=DEFAULT_TIMEOUT):
|
||||
"""
|
||||
Waits on all commands up to given timeout
|
||||
:param commands: list of commands (of type Cmd)
|
||||
:param timeout: Timeout in seconds for command.
|
||||
:return: commands and their results (tuple of Command and CmdResult)
|
||||
"""
|
||||
init_time = time.time()
|
||||
curr_time = init_time
|
||||
|
||||
results = []
|
||||
|
||||
while (curr_time - init_time < timeout) and (len(commands) != 0):
|
||||
for command in list(commands): # list(commands) clones the list. We do so because we remove items inside
|
||||
CmdRunner._process_command(command, commands, results, True)
|
||||
|
||||
time.sleep(CmdRunner.WAIT_SLEEP_TIME)
|
||||
curr_time = time.time()
|
||||
|
||||
for command in list(commands):
|
||||
CmdRunner._process_command(command, commands, results, False)
|
||||
|
||||
for command, result in results:
|
||||
if not result.is_success:
|
||||
logger.error('The following command failed: `%s`. status code: %s',
|
||||
str(command[1]), str(result.status_code))
|
||||
|
||||
return results
|
||||
|
||||
@abstractmethod
|
||||
def query_command(self, command_id):
|
||||
"""
|
||||
Queries the already run command for more info
|
||||
:param command_id: The command ID to query
|
||||
:return: Command info (in any format)
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
def get_command_result(self, command_info):
|
||||
"""
|
||||
Gets the result of the already run command
|
||||
:param command_info: The command info of the command to get the result of
|
||||
:return: CmdResult
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
def get_command_status(self, command_info):
|
||||
"""
|
||||
Gets the status of the already run command
|
||||
:param command_info: The command info of the command to get the result of
|
||||
:return: CmdStatus
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@staticmethod
|
||||
def _process_command(command, commands, results, should_process_only_finished):
|
||||
"""
|
||||
Removes the command from the list, processes its result and appends to results
|
||||
:param command: Command to process. Must be in commands.
|
||||
:param commands: List of unprocessed commands.
|
||||
:param results: List of command results.
|
||||
:param should_process_only_finished: If True, processes only if command finished.
|
||||
:return: None
|
||||
"""
|
||||
c_runner = command.cmd_runner
|
||||
c_id = command.cmd_id
|
||||
try:
|
||||
command_info = c_runner.query_command(c_id)
|
||||
if (not should_process_only_finished) or c_runner.get_command_status(command_info) != CmdStatus.IN_PROGRESS:
|
||||
commands.remove(command)
|
||||
results.append((command, c_runner.get_command_result(command_info)))
|
||||
except Exception:
|
||||
logger.exception('Exception while querying command: `%s`', str(c_id))
|
||||
if not should_process_only_finished:
|
||||
commands.remove(command)
|
||||
results.append((command, CmdResult(False)))
|
|
@ -0,0 +1,9 @@
|
|||
from enum import Enum
|
||||
|
||||
__author__ = 'itay.mizeretz'
|
||||
|
||||
|
||||
class CmdStatus(Enum):
|
||||
IN_PROGRESS = 0
|
||||
SUCCESS = 1
|
||||
FAILURE = 2
|
|
@ -1,6 +1,6 @@
|
|||
import logging
|
||||
|
||||
from common.cloud.aws import AWS
|
||||
from common.cloud.aws_instance import AwsInstance
|
||||
|
||||
__author__ = 'itay.mizeretz'
|
||||
|
||||
|
@ -15,7 +15,7 @@ class AwsCollector(object):
|
|||
@staticmethod
|
||||
def get_aws_info():
|
||||
LOG.info("Collecting AWS info")
|
||||
aws = AWS()
|
||||
aws = AwsInstance()
|
||||
info = {}
|
||||
if aws.is_aws_instance():
|
||||
LOG.info("Machine is an AWS instance")
|
||||
|
|
|
@ -22,6 +22,7 @@ from cc.resources.island_configuration import IslandConfiguration
|
|||
from cc.resources.monkey_download import MonkeyDownload
|
||||
from cc.resources.netmap import NetMap
|
||||
from cc.resources.node import Node
|
||||
from cc.resources.remote_run import RemoteRun
|
||||
from cc.resources.report import Report
|
||||
from cc.resources.root import Root
|
||||
from cc.resources.telemetry import Telemetry
|
||||
|
@ -115,5 +116,6 @@ def init_app(mongo_url):
|
|||
api.add_resource(TelemetryFeed, '/api/telemetry-feed', '/api/telemetry-feed/')
|
||||
api.add_resource(Log, '/api/log', '/api/log/')
|
||||
api.add_resource(IslandLog, '/api/log/island/download', '/api/log/island/download/')
|
||||
api.add_resource(RemoteRun, '/api/remote-monkey', '/api/remote-monkey/')
|
||||
|
||||
return app
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
import cc.auth
|
||||
from cc.environment import Environment
|
||||
from common.cloud.aws import AWS
|
||||
from common.cloud.aws_instance import AwsInstance
|
||||
from Crypto.Hash import SHA3_512
|
||||
|
||||
__author__ = 'itay.mizeretz'
|
||||
|
||||
|
||||
class AwsEnvironment(Environment):
|
||||
def __init__(self):
|
||||
super(AwsEnvironment, self).__init__()
|
||||
self.aws_info = AWS()
|
||||
self.aws_info = AwsInstance()
|
||||
self._instance_id = self._get_instance_id()
|
||||
self.region = self._get_region()
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ from botocore.exceptions import UnknownServiceError
|
|||
from cc.resources.exporter import Exporter
|
||||
from cc.services.config import ConfigService
|
||||
from cc.environment.environment import load_server_configuration_from_file
|
||||
from common.cloud.aws import AWS
|
||||
from common.cloud.aws_instance import AwsInstance
|
||||
|
||||
__author__ = 'maor.rayzin'
|
||||
|
||||
|
@ -23,7 +23,7 @@ class AWSExporter(Exporter):
|
|||
|
||||
@staticmethod
|
||||
def handle_report(report_json):
|
||||
aws = AWS()
|
||||
aws = AwsInstance()
|
||||
findings_list = []
|
||||
issues_list = report_json['recommendations']['issues']
|
||||
if not issues_list:
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
import json
|
||||
from flask import request, jsonify, make_response
|
||||
import flask_restful
|
||||
|
||||
from cc.auth import jwt_required
|
||||
from cc.services.remote_run_aws import RemoteRunAwsService
|
||||
from common.cloud.aws_service import AwsService
|
||||
|
||||
|
||||
class RemoteRun(flask_restful.Resource):
|
||||
def __init__(self):
|
||||
super(RemoteRun, self).__init__()
|
||||
RemoteRunAwsService.init()
|
||||
|
||||
def run_aws_monkeys(self, request_body):
|
||||
instances = request_body.get('instances')
|
||||
island_ip = request_body.get('island_ip')
|
||||
return RemoteRunAwsService.run_aws_monkeys(instances, island_ip)
|
||||
|
||||
@jwt_required()
|
||||
def get(self):
|
||||
action = request.args.get('action')
|
||||
if action == 'list_aws':
|
||||
is_aws = RemoteRunAwsService.is_running_on_aws()
|
||||
resp = {'is_aws': is_aws}
|
||||
if is_aws:
|
||||
is_auth = RemoteRunAwsService.update_aws_auth_params()
|
||||
resp['auth'] = is_auth
|
||||
if is_auth:
|
||||
resp['instances'] = AwsService.get_instances()
|
||||
return jsonify(resp)
|
||||
|
||||
return {}
|
||||
|
||||
@jwt_required()
|
||||
def post(self):
|
||||
body = json.loads(request.data)
|
||||
resp = {}
|
||||
if body.get('type') == 'aws':
|
||||
is_auth = RemoteRunAwsService.update_aws_auth_params()
|
||||
resp['auth'] = is_auth
|
||||
if is_auth:
|
||||
result = self.run_aws_monkeys(body)
|
||||
resp['result'] = result
|
||||
return jsonify(resp)
|
||||
|
||||
# default action
|
||||
return make_response({'error': 'Invalid action'}, 500)
|
|
@ -0,0 +1,138 @@
|
|||
from cc.services.config import ConfigService
|
||||
from common.cloud.aws_instance import AwsInstance
|
||||
from common.cloud.aws_service import AwsService
|
||||
from common.cmd.aws.aws_cmd_runner import AwsCmdRunner
|
||||
from common.cmd.cmd import Cmd
|
||||
from common.cmd.cmd_runner import CmdRunner
|
||||
|
||||
__author__ = "itay.mizeretz"
|
||||
|
||||
|
||||
class RemoteRunAwsService:
|
||||
aws_instance = None
|
||||
is_auth = False
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def init():
|
||||
"""
|
||||
Initializes service. Subsequent calls to this function have no effect.
|
||||
Must be called at least once (in entire monkey lifetime) before usage of functions
|
||||
:return: None
|
||||
"""
|
||||
if RemoteRunAwsService.aws_instance is None:
|
||||
RemoteRunAwsService.aws_instance = AwsInstance()
|
||||
|
||||
@staticmethod
|
||||
def run_aws_monkeys(instances, island_ip):
|
||||
"""
|
||||
Runs monkeys on the given instances
|
||||
:param instances: List of instances to run on
|
||||
:param island_ip: IP of island the monkey will communicate with
|
||||
:return: Dictionary with instance ids as keys, and True/False as values if succeeded or not
|
||||
"""
|
||||
instances_bitness = RemoteRunAwsService.get_bitness(instances)
|
||||
return CmdRunner.run_multiple_commands(
|
||||
instances,
|
||||
lambda instance: RemoteRunAwsService.run_aws_monkey_cmd_async(
|
||||
instance['instance_id'], RemoteRunAwsService._is_linux(instance['os']), island_ip,
|
||||
instances_bitness[instance['instance_id']]),
|
||||
lambda _, result: result.is_success)
|
||||
|
||||
@staticmethod
|
||||
def is_running_on_aws():
|
||||
return RemoteRunAwsService.aws_instance.is_aws_instance()
|
||||
|
||||
@staticmethod
|
||||
def update_aws_auth_params():
|
||||
"""
|
||||
Updates the AWS authentication parameters according to config
|
||||
:return: True if new params allow successful authentication. False otherwise
|
||||
"""
|
||||
access_key_id = ConfigService.get_config_value(['cnc', 'aws_config', 'aws_access_key_id'], False, True)
|
||||
secret_access_key = ConfigService.get_config_value(['cnc', 'aws_config', 'aws_secret_access_key'], False, True)
|
||||
|
||||
if (access_key_id != AwsService.access_key_id) or (secret_access_key != AwsService.secret_access_key):
|
||||
AwsService.set_auth_params(access_key_id, secret_access_key)
|
||||
RemoteRunAwsService.is_auth = AwsService.test_client()
|
||||
|
||||
AwsService.set_region(RemoteRunAwsService.aws_instance.region)
|
||||
|
||||
return RemoteRunAwsService.is_auth
|
||||
|
||||
@staticmethod
|
||||
def get_bitness(instances):
|
||||
"""
|
||||
For all given instances, checks whether they're 32 or 64 bit.
|
||||
:param instances: List of instances to check
|
||||
:return: Dictionary with instance ids as keys, and True/False as values. True if 64bit, False otherwise
|
||||
"""
|
||||
return CmdRunner.run_multiple_commands(
|
||||
instances,
|
||||
lambda instance: RemoteRunAwsService.run_aws_bitness_cmd_async(
|
||||
instance['instance_id'], RemoteRunAwsService._is_linux(instance['os'])),
|
||||
lambda instance, result: RemoteRunAwsService._get_bitness_by_result(
|
||||
RemoteRunAwsService._is_linux(instance['os']), result))
|
||||
|
||||
@staticmethod
|
||||
def _get_bitness_by_result(is_linux, result):
|
||||
if not result.is_success:
|
||||
return None
|
||||
elif is_linux:
|
||||
return result.stdout.find('i686') == -1 # i686 means 32bit
|
||||
else:
|
||||
return result.stdout.lower().find('programfiles(x86)') != -1 # if not found it means 32bit
|
||||
|
||||
@staticmethod
|
||||
def run_aws_bitness_cmd_async(instance_id, is_linux):
|
||||
"""
|
||||
Runs an AWS command to check bitness
|
||||
:param instance_id: Instance ID of target
|
||||
:param is_linux: Whether target is linux
|
||||
:return: Cmd
|
||||
"""
|
||||
cmd_text = 'uname -m' if is_linux else 'Get-ChildItem Env:'
|
||||
return RemoteRunAwsService.run_aws_cmd_async(instance_id, is_linux, cmd_text)
|
||||
|
||||
@staticmethod
|
||||
def run_aws_monkey_cmd_async(instance_id, is_linux, island_ip, is_64bit):
|
||||
"""
|
||||
Runs a monkey remotely using AWS
|
||||
:param instance_id: Instance ID of target
|
||||
:param is_linux: Whether target is linux
|
||||
:param island_ip: IP of the island which the instance will try to connect to
|
||||
:param is_64bit: Whether the instance is 64bit
|
||||
:return: Cmd
|
||||
"""
|
||||
cmd_text = RemoteRunAwsService._get_run_monkey_cmd_line(is_linux, is_64bit, island_ip)
|
||||
return RemoteRunAwsService.run_aws_cmd_async(instance_id, is_linux, cmd_text)
|
||||
|
||||
@staticmethod
|
||||
def run_aws_cmd_async(instance_id, is_linux, cmd_line):
|
||||
cmd_runner = AwsCmdRunner(is_linux, instance_id)
|
||||
return Cmd(cmd_runner, cmd_runner.run_command_async(cmd_line))
|
||||
|
||||
@staticmethod
|
||||
def _is_linux(os):
|
||||
return 'linux' == os
|
||||
|
||||
@staticmethod
|
||||
def _get_run_monkey_cmd_linux_line(bit_text, island_ip):
|
||||
return r'wget --no-check-certificate https://' + island_ip + r':5000/api/monkey/download/monkey-linux-' + \
|
||||
bit_text + r'; chmod +x monkey-linux-' + bit_text + r'; ./monkey-linux-' + bit_text + r' m0nk3y -s ' + \
|
||||
island_ip + r':5000'
|
||||
|
||||
@staticmethod
|
||||
def _get_run_monkey_cmd_windows_line(bit_text, island_ip):
|
||||
return r"[System.Net.ServicePointManager]::ServerCertificateValidationCallback = {" \
|
||||
r"$true}; (New-Object System.Net.WebClient).DownloadFile('https://" + island_ip + \
|
||||
r":5000/api/monkey/download/monkey-windows-" + bit_text + r".exe','.\\monkey.exe'); " \
|
||||
r";Start-Process -FilePath '.\\monkey.exe' -ArgumentList 'm0nk3y -s " + island_ip + r":5000'; "
|
||||
|
||||
@staticmethod
|
||||
def _get_run_monkey_cmd_line(is_linux, is_64bit, island_ip):
|
||||
bit_text = '64' if is_64bit else '32'
|
||||
return RemoteRunAwsService._get_run_monkey_cmd_linux_line(bit_text, island_ip) if is_linux \
|
||||
else RemoteRunAwsService._get_run_monkey_cmd_windows_line(bit_text, island_ip)
|
|
@ -1,9 +1,10 @@
|
|||
import React from 'react';
|
||||
import {Button, Col, Well, Nav, NavItem, Collapse} from 'react-bootstrap';
|
||||
import {Button, Col, Well, Nav, NavItem, Collapse, Form, FormControl, FormGroup} from 'react-bootstrap';
|
||||
import CopyToClipboard from 'react-copy-to-clipboard';
|
||||
import {Icon} from 'react-fa';
|
||||
import {Link} from 'react-router-dom';
|
||||
import AuthComponent from '../AuthComponent';
|
||||
import AwsRunTable from "../run-monkey/AwsRunTable";
|
||||
|
||||
class RunMonkeyPageComponent extends AuthComponent {
|
||||
|
||||
|
@ -13,10 +14,19 @@ class RunMonkeyPageComponent extends AuthComponent {
|
|||
ips: [],
|
||||
runningOnIslandState: 'not_running',
|
||||
runningOnClientState: 'not_running',
|
||||
awsClicked: false,
|
||||
selectedIp: '0.0.0.0',
|
||||
selectedOs: 'windows-32',
|
||||
showManual: false
|
||||
};
|
||||
showManual: false,
|
||||
showAws: false,
|
||||
isOnAws: false,
|
||||
isAwsAuth: false,
|
||||
awsUpdateClicked: false,
|
||||
awsUpdateFailed: false,
|
||||
awsKeyId: '',
|
||||
awsSecretKey: '',
|
||||
awsMachines: []
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
|
@ -37,6 +47,15 @@ class RunMonkeyPageComponent extends AuthComponent {
|
|||
}
|
||||
});
|
||||
|
||||
this.fetchAwsInfo();
|
||||
this.fetchConfig()
|
||||
.then(config => {
|
||||
this.setState({
|
||||
awsKeyId: config['cnc']['aws_config']['aws_access_key_id'],
|
||||
awsSecretKey: config['cnc']['aws_config']['aws_secret_access_key']
|
||||
});
|
||||
});
|
||||
|
||||
this.authFetch('/api/client-monkey')
|
||||
.then(res => res.json())
|
||||
.then(res => {
|
||||
|
@ -50,6 +69,17 @@ class RunMonkeyPageComponent extends AuthComponent {
|
|||
this.props.onStatusChange();
|
||||
}
|
||||
|
||||
fetchAwsInfo() {
|
||||
return this.authFetch('/api/remote-monkey?action=list_aws')
|
||||
.then(res => res.json())
|
||||
.then(res =>{
|
||||
let is_aws = res['is_aws'];
|
||||
if (is_aws) {
|
||||
this.setState({isOnAws: true, awsMachines: res['instances'], isAwsAuth: res['auth']});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
generateLinuxCmd(ip, is32Bit) {
|
||||
let bitText = is32Bit ? '32' : '64';
|
||||
return `wget --no-check-certificate https://${ip}:5000/api/monkey/download/monkey-linux-${bitText}; chmod +x monkey-linux-${bitText}; ./monkey-linux-${bitText} m0nk3y -s ${ip}:5000`
|
||||
|
@ -134,6 +164,193 @@ class RunMonkeyPageComponent extends AuthComponent {
|
|||
});
|
||||
};
|
||||
|
||||
toggleAws = () => {
|
||||
this.setState({
|
||||
showAws: !this.state.showAws
|
||||
});
|
||||
};
|
||||
|
||||
runOnAws = () => {
|
||||
this.setState({
|
||||
awsClicked: true
|
||||
});
|
||||
|
||||
let instances = this.awsTable.state.selection.map(x => this.instanceIdToInstance(x));
|
||||
|
||||
this.authFetch('/api/remote-monkey',
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({type: 'aws', instances: instances, island_ip: this.state.selectedIp})
|
||||
}).then(res => res.json())
|
||||
.then(res => {
|
||||
let result = res['result'];
|
||||
|
||||
// update existing state, not run-over
|
||||
let prevRes = this.awsTable.state.result;
|
||||
for (let key in result) {
|
||||
if (result.hasOwnProperty(key)) {
|
||||
prevRes[key] = result[key];
|
||||
}
|
||||
}
|
||||
this.awsTable.setState({
|
||||
result: prevRes,
|
||||
selection: [],
|
||||
selectAll: false
|
||||
});
|
||||
|
||||
this.setState({
|
||||
awsClicked: false
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
updateAwsKeyId = (evt) => {
|
||||
this.setState({
|
||||
awsKeyId: evt.target.value
|
||||
});
|
||||
};
|
||||
|
||||
updateAwsSecretKey = (evt) => {
|
||||
this.setState({
|
||||
awsSecretKey: evt.target.value
|
||||
});
|
||||
};
|
||||
|
||||
fetchConfig() {
|
||||
return this.authFetch('/api/configuration/island')
|
||||
.then(res => res.json())
|
||||
.then(res => {
|
||||
return res.configuration;
|
||||
})
|
||||
}
|
||||
|
||||
updateAwsKeys = () => {
|
||||
this.setState({
|
||||
awsUpdateClicked: true,
|
||||
awsUpdateFailed: false
|
||||
});
|
||||
this.fetchConfig()
|
||||
.then(config => {
|
||||
let new_config = config;
|
||||
new_config['cnc']['aws_config']['aws_access_key_id'] = this.state.awsKeyId;
|
||||
new_config['cnc']['aws_config']['aws_secret_access_key'] = this.state.awsSecretKey;
|
||||
return new_config;
|
||||
})
|
||||
.then(new_config => {
|
||||
this.authFetch('/api/configuration/island',
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify(new_config)
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(res => {
|
||||
this.fetchAwsInfo()
|
||||
.then(res => {
|
||||
if (!this.state.isAwsAuth) {
|
||||
this.setState({
|
||||
awsUpdateClicked: false,
|
||||
awsUpdateFailed: true
|
||||
})
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
instanceIdToInstance = (instance_id) => {
|
||||
let instance = this.state.awsMachines.find(
|
||||
function (inst) {
|
||||
return inst['instance_id'] === instance_id;
|
||||
});
|
||||
return {'instance_id': instance_id, 'os': instance['os']}
|
||||
|
||||
};
|
||||
|
||||
renderAuthAwsDiv() {
|
||||
return (
|
||||
<div style={{'marginBottom': '2em'}}>
|
||||
<p style={{'fontSize': '1.2em'}}>
|
||||
Select Island IP address
|
||||
</p>
|
||||
{
|
||||
this.state.ips.length > 1 ?
|
||||
<Nav bsStyle="pills" justified activeKey={this.state.selectedIp} onSelect={this.setSelectedIp}
|
||||
style={{'marginBottom': '2em'}}>
|
||||
{this.state.ips.map(ip => <NavItem key={ip} eventKey={ip}>{ip}</NavItem>)}
|
||||
</Nav>
|
||||
: <div style={{'marginBottom': '2em'}} />
|
||||
}
|
||||
|
||||
<AwsRunTable
|
||||
data={this.state.awsMachines}
|
||||
ref={r => (this.awsTable = r)}
|
||||
/>
|
||||
<div style={{'marginTop': '1em'}}>
|
||||
<button
|
||||
onClick={this.runOnAws}
|
||||
className={'btn btn-default btn-md center-block'}
|
||||
disabled={this.state.awsClicked}>
|
||||
Run on selected machines
|
||||
{ this.state.awsClicked ? <Icon name="refresh" className="text-success" style={{'marginLeft': '5px'}}/> : null }
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
renderNotAuthAwsDiv() {
|
||||
return (
|
||||
<div style={{'marginBottom': '2em'}}>
|
||||
<p style={{'fontSize': '1.2em'}}>
|
||||
You haven't set your AWS account details or they're incorrect. Please enter them below to proceed.
|
||||
</p>
|
||||
<div style={{'marginTop': '1em'}}>
|
||||
<div className="col-sm-12">
|
||||
<div className="col-sm-6 col-sm-offset-3" style={{'fontSize': '1.2em'}}>
|
||||
<div className="panel panel-default">
|
||||
<div className="panel-body">
|
||||
<div className="input-group center-block text-center">
|
||||
<input type="text" className="form-control" placeholder="AWS Access Key ID"
|
||||
value={this.state.awsKeyId}
|
||||
onChange={evt => this.updateAwsKeyId(evt)}/>
|
||||
<input type="text" className="form-control" placeholder="AWS Secret Access Key"
|
||||
value={this.state.awsSecretKey}
|
||||
onChange={evt => this.updateAwsSecretKey(evt)}/>
|
||||
<Button
|
||||
onClick={this.updateAwsKeys}
|
||||
className={'btn btn-default btn-md center-block'}
|
||||
disabled={this.state.awsUpdateClicked}
|
||||
variant="primary">
|
||||
Update AWS details
|
||||
{ this.state.awsUpdateClicked ? <Icon name="refresh" className="text-success" style={{'marginLeft': '5px'}}/> : null }
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
this.state.awsUpdateFailed ?
|
||||
<div className="col-sm-8 col-sm-offset-2" style={{'fontSize': '1.2em'}}>
|
||||
<p className="alert alert-danger" role="alert">Authentication failed.</p>
|
||||
<p className="alert alert-info">
|
||||
<i className="glyphicon glyphicon-info-sign" style={{'marginRight': '5px'}}/>
|
||||
In order to remotely run commands on AWS EC2 instances, please make sure you have
|
||||
the <a href="https://docs.aws.amazon.com/console/ec2/run-command/prereqs" target="_blank">prerequisites</a> and if the
|
||||
instances don't show up, check the
|
||||
AWS <a href="https://docs.aws.amazon.com/console/ec2/run-command/troubleshooting" target="_blank">troubleshooting guide</a>.
|
||||
</p>
|
||||
</div>
|
||||
:
|
||||
null
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Col xs={12} lg={8}>
|
||||
|
@ -166,7 +383,7 @@ class RunMonkeyPageComponent extends AuthComponent {
|
|||
<p className="text-center">
|
||||
OR
|
||||
</p>
|
||||
<p style={{'marginBottom': '2em'}}>
|
||||
<p style={this.state.showManual || !this.state.isOnAws ? {'marginBottom': '2em'} : {}}>
|
||||
<button onClick={this.toggleManual} className={'btn btn-default btn-lg center-block' + (this.state.showManual ? ' active' : '')}>
|
||||
Run on machine of your choice
|
||||
</button>
|
||||
|
@ -196,6 +413,30 @@ class RunMonkeyPageComponent extends AuthComponent {
|
|||
{this.generateCmdDiv()}
|
||||
</div>
|
||||
</Collapse>
|
||||
{
|
||||
this.state.isOnAws ?
|
||||
<p className="text-center">
|
||||
OR
|
||||
</p>
|
||||
:
|
||||
null
|
||||
}
|
||||
{
|
||||
this.state.isOnAws ?
|
||||
<p style={{'marginBottom': '2em'}}>
|
||||
<button onClick={this.toggleAws} className={'btn btn-default btn-lg center-block' + (this.state.showAws ? ' active' : '')}>
|
||||
Run on AWS machine of your choice
|
||||
</button>
|
||||
</p>
|
||||
:
|
||||
null
|
||||
}
|
||||
<Collapse in={this.state.showAws}>
|
||||
{
|
||||
this.state.isAwsAuth ? this.renderAuthAwsDiv() : this.renderNotAuthAwsDiv()
|
||||
}
|
||||
|
||||
</Collapse>
|
||||
|
||||
<p style={{'fontSize': '1.2em'}}>
|
||||
Go ahead and monitor the ongoing infection in the <Link to="/infection/map">Infection Map</Link> view.
|
||||
|
|
|
@ -0,0 +1,109 @@
|
|||
import React from 'react';
|
||||
import ReactTable from 'react-table'
|
||||
import checkboxHOC from "react-table/lib/hoc/selectTable";
|
||||
|
||||
const CheckboxTable = checkboxHOC(ReactTable);
|
||||
|
||||
const columns = [
|
||||
{
|
||||
Header: 'Machines',
|
||||
columns: [
|
||||
{ Header: 'Machine', accessor: 'name'},
|
||||
{ Header: 'Instance ID', accessor: 'instance_id'},
|
||||
{ Header: 'IP Address', accessor: 'ip_address'},
|
||||
{ Header: 'OS', accessor: 'os'}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
const pageSize = 10;
|
||||
|
||||
class AwsRunTableComponent extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
selection: [],
|
||||
selectAll: false,
|
||||
result: {}
|
||||
}
|
||||
}
|
||||
|
||||
toggleSelection = (key, shift, row) => {
|
||||
// start off with the existing state
|
||||
let selection = [...this.state.selection];
|
||||
const keyIndex = selection.indexOf(key);
|
||||
// check to see if the key exists
|
||||
if (keyIndex >= 0) {
|
||||
// it does exist so we will remove it using destructing
|
||||
selection = [
|
||||
...selection.slice(0, keyIndex),
|
||||
...selection.slice(keyIndex + 1)
|
||||
];
|
||||
} else {
|
||||
// it does not exist so add it
|
||||
selection.push(key);
|
||||
}
|
||||
// update the state
|
||||
this.setState({ selection });
|
||||
};
|
||||
|
||||
isSelected = key => {
|
||||
return this.state.selection.includes(key);
|
||||
};
|
||||
|
||||
toggleAll = () => {
|
||||
const selectAll = !this.state.selectAll;
|
||||
const selection = [];
|
||||
if (selectAll) {
|
||||
// we need to get at the internals of ReactTable
|
||||
const wrappedInstance = this.checkboxTable.getWrappedInstance();
|
||||
// the 'sortedData' property contains the currently accessible records based on the filter and sort
|
||||
const currentRecords = wrappedInstance.getResolvedState().sortedData;
|
||||
// we just push all the IDs onto the selection array
|
||||
currentRecords.forEach(item => {
|
||||
selection.push(item._original.instance_id);
|
||||
});
|
||||
}
|
||||
this.setState({ selectAll, selection });
|
||||
};
|
||||
|
||||
getTrProps = (s, r) => {
|
||||
let color = "inherit";
|
||||
if (r) {
|
||||
let instId = r.original.instance_id;
|
||||
if (this.isSelected(instId)) {
|
||||
color = "#ffed9f";
|
||||
} else if (this.state.result.hasOwnProperty(instId)) {
|
||||
color = this.state.result[instId] ? "#00f01b" : '#f00000'
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
style: {backgroundColor: color}
|
||||
};
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="data-table-container">
|
||||
<CheckboxTable
|
||||
ref={r => (this.checkboxTable = r)}
|
||||
keyField="instance_id"
|
||||
columns={columns}
|
||||
data={this.props.data}
|
||||
showPagination={true}
|
||||
defaultPageSize={pageSize}
|
||||
className="-highlight"
|
||||
selectType="checkbox"
|
||||
toggleSelection={this.toggleSelection}
|
||||
isSelected={this.isSelected}
|
||||
toggleAll={this.toggleAll}
|
||||
selectAll={this.state.selectAll}
|
||||
getTrProps={this.getTrProps}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default AwsRunTableComponent;
|
Loading…
Reference in New Issue