diff --git a/monkey/common/__init__.py b/monkey/common/__init__.py index 5f2cba2f1..01be39cf4 100644 --- a/monkey/common/__init__.py +++ b/monkey/common/__init__.py @@ -1 +1 @@ -from .di_container import DIContainer +from .di_container import DIContainer, UnregisteredTypeError diff --git a/monkey/common/aws/__init__.py b/monkey/common/aws/__init__.py index e69de29bb..1bf4dafaa 100644 --- a/monkey/common/aws/__init__.py +++ b/monkey/common/aws/__init__.py @@ -0,0 +1 @@ +from .aws_instance import AWSInstance diff --git a/monkey/common/aws/aws_instance.py b/monkey/common/aws/aws_instance.py index d99c87117..76c3cac9a 100644 --- a/monkey/common/aws/aws_instance.py +++ b/monkey/common/aws/aws_instance.py @@ -1,117 +1,52 @@ -import json -import logging -import re -from dataclasses import dataclass -from typing import Optional, Tuple +import threading -import requests +from .aws_metadata import fetch_aws_instance_metadata -AWS_INSTANCE_METADATA_LOCAL_IP_ADDRESS = "169.254.169.254" -AWS_LATEST_METADATA_URI_PREFIX = "http://{0}/latest/".format(AWS_INSTANCE_METADATA_LOCAL_IP_ADDRESS) -ACCOUNT_ID_KEY = "accountId" - -logger = logging.getLogger(__name__) - -AWS_TIMEOUT = 2 +AWS_FETCH_METADATA_TIMEOUT = 10.0 # Seconds -@dataclass -class AwsInstanceInfo: - instance_id: Optional[str] = None - region: Optional[str] = None - account_id: Optional[str] = None +class AWSTimeoutError(Exception): + """Raised when communications with AWS timeout""" -class AwsInstance: +class AWSInstance: """ Class which gives useful information about the current instance you're on. """ def __init__(self): - self._is_instance, self._instance_info = AwsInstance._fetch_instance_info() + self._instance_id = None + self._region = None + self._account_id = None + self._initialization_complete = threading.Event() + + fetch_thread = threading.Thread(target=self._fetch_aws_instance_metadata, daemon=True) + fetch_thread.start() + + def _fetch_aws_instance_metadata(self): + (self._instance_id, self._region, self._account_id) = fetch_aws_instance_metadata() + self._initialization_complete.set() @property def is_instance(self) -> bool: - return self._is_instance + self._wait_for_initialization_to_complete() + return bool(self._instance_id) @property def instance_id(self) -> str: - return self._instance_info.instance_id + self._wait_for_initialization_to_complete() + return self._instance_id @property def region(self) -> str: - return self._instance_info.region + self._wait_for_initialization_to_complete() + return self._region @property def account_id(self) -> str: - return self._instance_info.account_id + self._wait_for_initialization_to_complete() + return self._account_id - @staticmethod - def _fetch_instance_info() -> Tuple[bool, AwsInstanceInfo]: - try: - response = requests.get( - AWS_LATEST_METADATA_URI_PREFIX + "meta-data/instance-id", - timeout=AWS_TIMEOUT, - ) - if not response: - return False, AwsInstanceInfo() - - info = AwsInstanceInfo() - info.instance_id = response.text if response else False - info.region = AwsInstance._parse_region( - requests.get( - AWS_LATEST_METADATA_URI_PREFIX + "meta-data/placement/availability-zone", - timeout=AWS_TIMEOUT, - ).text - ) - except (requests.RequestException, IOError) as e: - logger.debug("Failed init of AwsInstance while getting metadata: {}".format(e)) - return False, AwsInstanceInfo() - - try: - info.account_id = AwsInstance._extract_account_id( - requests.get( - AWS_LATEST_METADATA_URI_PREFIX + "dynamic/instance-identity/document", - timeout=AWS_TIMEOUT, - ).text - ) - except (requests.RequestException, json.decoder.JSONDecodeError, IOError) as e: - logger.debug( - "Failed init of AwsInstance while getting dynamic instance data: {}".format(e) - ) - return False, AwsInstanceInfo() - - return True, info - - @staticmethod - def _parse_region(region_url_response): - # For a list of regions, see: - # https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Concepts - # .RegionsAndAvailabilityZones.html - # This regex will find any AWS region format string in the response. - re_phrase = r"((?:us|eu|ap|ca|cn|sa)-[a-z]*-[0-9])" - finding = re.findall(re_phrase, region_url_response, re.IGNORECASE) - if finding: - return finding[0] - else: - return None - - @staticmethod - def _extract_account_id(instance_identity_document_response): - """ - Extracts the account id from the dynamic/instance-identity/document metadata path. - Based on https://forums.aws.amazon.com/message.jspa?messageID=409028 which has a few more - solutions, - in case Amazon break this mechanism. - :param instance_identity_document_response: json returned via the web page - ../dynamic/instance-identity/document - :return: The account id - """ - return json.loads(instance_identity_document_response)[ACCOUNT_ID_KEY] - - def get_account_id(self): - """ - :return: the AWS account ID which "owns" this instance. - See https://docs.aws.amazon.com/general/latest/gr/acct-identifiers.html - """ - return self.account_id + def _wait_for_initialization_to_complete(self): + if not self._initialization_complete.wait(AWS_FETCH_METADATA_TIMEOUT): + raise AWSTimeoutError("Timed out while attempting to retrieve metadata from AWS") diff --git a/monkey/common/aws/aws_metadata.py b/monkey/common/aws/aws_metadata.py new file mode 100644 index 000000000..634d41c49 --- /dev/null +++ b/monkey/common/aws/aws_metadata.py @@ -0,0 +1,86 @@ +import json +import logging +import re +from typing import Optional, Tuple + +import requests + +AWS_INSTANCE_METADATA_LOCAL_IP_ADDRESS = "169.254.169.254" +AWS_LATEST_METADATA_URI_PREFIX = f"http://{AWS_INSTANCE_METADATA_LOCAL_IP_ADDRESS}/latest/" +ACCOUNT_ID_KEY = "accountId" + +logger = logging.getLogger(__name__) + +AWS_TIMEOUT = 2 + + +def fetch_aws_instance_metadata() -> Tuple[Optional[str], Optional[str], Optional[str]]: + instance_id = None + region = None + account_id = None + + try: + instance_id = _fetch_aws_instance_id() + region = _fetch_aws_region() + account_id = _fetch_account_id() + except ( + requests.RequestException, + IOError, + json.decoder.JSONDecodeError, + ) as err: + logger.debug(f"Failed init of AWSInstance while getting metadata: {err}") + return (None, None, None) + + return (instance_id, region, account_id) + + +def _fetch_aws_instance_id() -> Optional[str]: + url = AWS_LATEST_METADATA_URI_PREFIX + "meta-data/instance-id" + response = requests.get( + url, + timeout=AWS_TIMEOUT, + ) + response.raise_for_status() + + return response.text + + +def _fetch_aws_region() -> Optional[str]: + response = requests.get( + AWS_LATEST_METADATA_URI_PREFIX + "meta-data/placement/availability-zone", + timeout=AWS_TIMEOUT, + ) + response.raise_for_status() + + return _parse_region(response.text) + + +def _parse_region(region_url_response: str) -> Optional[str]: + # For a list of regions, see: + # https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Concepts + # .RegionsAndAvailabilityZones.html + # This regex will find any AWS region format string in the response. + re_phrase = r"((?:us|eu|ap|ca|cn|sa)-[a-z]*-[0-9])" + finding = re.findall(re_phrase, region_url_response, re.IGNORECASE) + if finding: + return finding[0] + else: + return None + + +def _fetch_account_id() -> str: + """ + Fetches and extracts the account id from the dynamic/instance-identity/document metadata path. + Based on https://forums.aws.amazon.com/message.jspa?messageID=409028 which has a few more + solutions, in case Amazon break this mechanism. + :param instance_identity_document_response: json returned via the web page + ../dynamic/instance-identity/document + :return: The account id + """ + response = requests.get( + AWS_LATEST_METADATA_URI_PREFIX + "dynamic/instance-identity/document", + timeout=AWS_TIMEOUT, + ) + response.raise_for_status() + + return json.loads(response.text)[ACCOUNT_ID_KEY] diff --git a/monkey/common/cmd/__init__.py b/monkey/common/cmd/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/monkey/common/cmd/aws/__init__.py b/monkey/common/cmd/aws/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/monkey/common/cmd/cmd.py b/monkey/common/cmd/cmd.py deleted file mode 100644 index 6daa970a6..000000000 --- a/monkey/common/cmd/cmd.py +++ /dev/null @@ -1,8 +0,0 @@ -class Cmd(object): - """ - Class representing a command - """ - - def __init__(self, cmd_runner, cmd_id): - self.cmd_runner = cmd_runner - self.cmd_id = cmd_id diff --git a/monkey/common/cmd/cmd_result.py b/monkey/common/cmd/cmd_result.py deleted file mode 100644 index 85179a053..000000000 --- a/monkey/common/cmd/cmd_result.py +++ /dev/null @@ -1,10 +0,0 @@ -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 diff --git a/monkey/common/cmd/cmd_runner.py b/monkey/common/cmd/cmd_runner.py deleted file mode 100644 index 38d22b0a1..000000000 --- a/monkey/common/cmd/cmd_runner.py +++ /dev/null @@ -1,154 +0,0 @@ -import logging -import time -from abc import abstractmethod - -from common.cmd.cmd_result import CmdResult -from common.cmd.cmd_status import CmdStatus - -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 - - @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(list(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 = [] - # TODO: Use timer.Timer - 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( - f"The command with id: {str(command.cmd_id)} failed. " - f"Status code: {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))) diff --git a/monkey/common/cmd/cmd_status.py b/monkey/common/cmd/cmd_status.py deleted file mode 100644 index 6a9bbae71..000000000 --- a/monkey/common/cmd/cmd_status.py +++ /dev/null @@ -1,7 +0,0 @@ -from enum import Enum - - -class CmdStatus(Enum): - IN_PROGRESS = 0 - SUCCESS = 1 - FAILURE = 2 diff --git a/monkey/common/utils/__init__.py b/monkey/common/utils/__init__.py index e69de29bb..57725fa62 100644 --- a/monkey/common/utils/__init__.py +++ b/monkey/common/utils/__init__.py @@ -0,0 +1 @@ +from .timer import Timer diff --git a/monkey/common/utils/code_utils.py b/monkey/common/utils/code_utils.py index 810fbb0d7..251ce9375 100644 --- a/monkey/common/utils/code_utils.py +++ b/monkey/common/utils/code_utils.py @@ -1,3 +1,7 @@ +import queue +from typing import Any, List + + class abstractstatic(staticmethod): __slots__ = () @@ -15,3 +19,14 @@ class Singleton(type): if cls not in cls._instances: cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs) return cls._instances[cls] + + +def queue_to_list(q: queue.Queue) -> List[Any]: + list_ = [] + try: + while True: + list_.append(q.get_nowait()) + except queue.Empty: + pass + + return list_ diff --git a/monkey/infection_monkey/utils/timer.py b/monkey/common/utils/timer.py similarity index 100% rename from monkey/infection_monkey/utils/timer.py rename to monkey/common/utils/timer.py diff --git a/monkey/infection_monkey/exploit/log4shell.py b/monkey/infection_monkey/exploit/log4shell.py index e967ee6cb..dc8fe0c66 100644 --- a/monkey/infection_monkey/exploit/log4shell.py +++ b/monkey/infection_monkey/exploit/log4shell.py @@ -3,6 +3,7 @@ import time from pathlib import PurePath from common.common_consts.timeouts import LONG_REQUEST_TIMEOUT, MEDIUM_REQUEST_TIMEOUT +from common.utils import Timer from infection_monkey.exploit.log4shell_utils import ( LINUX_EXPLOIT_TEMPLATE_PATH, WINDOWS_EXPLOIT_TEMPLATE_PATH, @@ -21,7 +22,6 @@ from infection_monkey.network.tools import get_interface_to_target from infection_monkey.utils.commands import build_monkey_commandline from infection_monkey.utils.monkey_dir import get_monkey_dir_path from infection_monkey.utils.threading import interruptible_iter -from infection_monkey.utils.timer import Timer logger = logging.getLogger(__name__) diff --git a/monkey/infection_monkey/exploit/sshexec.py b/monkey/infection_monkey/exploit/sshexec.py index 2b13b719e..3c4c46641 100644 --- a/monkey/infection_monkey/exploit/sshexec.py +++ b/monkey/infection_monkey/exploit/sshexec.py @@ -5,6 +5,7 @@ from pathlib import PurePath import paramiko from common.common_consts.timeouts import LONG_REQUEST_TIMEOUT, MEDIUM_REQUEST_TIMEOUT +from common.utils import Timer from common.utils.attack_utils import ScanStatus from common.utils.exceptions import FailedExploitationError from infection_monkey.exploit.HostExploiter import HostExploiter @@ -17,7 +18,6 @@ from infection_monkey.telemetry.attack.t1222_telem import T1222Telem from infection_monkey.utils.brute_force import generate_identity_secret_pairs from infection_monkey.utils.commands import build_monkey_commandline from infection_monkey.utils.threading import interruptible_iter -from infection_monkey.utils.timer import Timer logger = logging.getLogger(__name__) SSH_PORT = 22 diff --git a/monkey/infection_monkey/master/automated_master.py b/monkey/infection_monkey/master/automated_master.py index b0ffa56fa..ac0429e1d 100644 --- a/monkey/infection_monkey/master/automated_master.py +++ b/monkey/infection_monkey/master/automated_master.py @@ -3,6 +3,7 @@ import threading import time from typing import Any, Callable, Dict, Iterable, List, Mapping, Optional, Tuple +from common.utils import Timer from infection_monkey.credential_store import ICredentialsStore from infection_monkey.i_control_channel import IControlChannel, IslandCommunicationError from infection_monkey.i_master import IMaster @@ -13,7 +14,6 @@ from infection_monkey.telemetry.credentials_telem import CredentialsTelem from infection_monkey.telemetry.messengers.i_telemetry_messenger import ITelemetryMessenger from infection_monkey.telemetry.post_breach_telem import PostBreachTelem from infection_monkey.utils.threading import create_daemon_thread, interruptible_iter -from infection_monkey.utils.timer import Timer from . import Exploiter, IPScanner, Propagator from .option_parsing import custom_pba_is_enabled diff --git a/monkey/infection_monkey/network_scanning/tcp_scanner.py b/monkey/infection_monkey/network_scanning/tcp_scanner.py index d0c6e3e7a..17c72f606 100644 --- a/monkey/infection_monkey/network_scanning/tcp_scanner.py +++ b/monkey/infection_monkey/network_scanning/tcp_scanner.py @@ -4,9 +4,9 @@ import socket import time from typing import Iterable, Mapping, Tuple +from common.utils import Timer from infection_monkey.i_puppet import PortScanData, PortStatus from infection_monkey.network.tools import BANNER_READ, DEFAULT_TIMEOUT, tcp_port_to_service -from infection_monkey.utils.timer import Timer logger = logging.getLogger(__name__) diff --git a/monkey/infection_monkey/telemetry/messengers/batching_telemetry_messenger.py b/monkey/infection_monkey/telemetry/messengers/batching_telemetry_messenger.py index 9dc051666..88c9d7d13 100644 --- a/monkey/infection_monkey/telemetry/messengers/batching_telemetry_messenger.py +++ b/monkey/infection_monkey/telemetry/messengers/batching_telemetry_messenger.py @@ -2,10 +2,10 @@ import queue import threading from typing import Dict +from common.utils import Timer from infection_monkey.telemetry.i_batchable_telem import IBatchableTelem from infection_monkey.telemetry.i_telem import ITelem from infection_monkey.telemetry.messengers.i_telemetry_messenger import ITelemetryMessenger -from infection_monkey.utils.timer import Timer DEFAULT_PERIOD = 5 WAKES_PER_PERIOD = 4 diff --git a/monkey/infection_monkey/tunnel.py b/monkey/infection_monkey/tunnel.py index 26368bff6..b4ca3b517 100644 --- a/monkey/infection_monkey/tunnel.py +++ b/monkey/infection_monkey/tunnel.py @@ -4,11 +4,11 @@ import struct import time from threading import Event, Thread +from common.utils import Timer from infection_monkey.network.firewall import app as firewall from infection_monkey.network.info import get_free_tcp_port, local_ips from infection_monkey.network.tools import check_tcp_port, get_interface_to_target from infection_monkey.transport.base import get_last_serve_time -from infection_monkey.utils.timer import Timer logger = logging.getLogger(__name__) diff --git a/monkey/infection_monkey/utils/aws_environment_check.py b/monkey/infection_monkey/utils/aws_environment_check.py index 203882425..dc074cd6a 100644 --- a/monkey/infection_monkey/utils/aws_environment_check.py +++ b/monkey/infection_monkey/utils/aws_environment_check.py @@ -1,6 +1,6 @@ import logging -from common.aws.aws_instance import AwsInstance +from common.aws import AWSInstance from infection_monkey.telemetry.aws_instance_telem import AWSInstanceTelemetry from infection_monkey.telemetry.messengers.legacy_telemetry_messenger_adapter import ( LegacyTelemetryMessengerAdapter, @@ -13,7 +13,7 @@ logger = logging.getLogger(__name__) def _report_aws_environment(telemetry_messenger: LegacyTelemetryMessengerAdapter): logger.info("Collecting AWS info") - aws_instance = AwsInstance() + aws_instance = AWSInstance() if aws_instance.is_instance: logger.info("Machine is an AWS instance") diff --git a/monkey/infection_monkey/utils/decorators.py b/monkey/infection_monkey/utils/decorators.py index 31ac0661b..363f29796 100644 --- a/monkey/infection_monkey/utils/decorators.py +++ b/monkey/infection_monkey/utils/decorators.py @@ -1,7 +1,7 @@ import threading from functools import wraps -from .timer import Timer +from common.utils import Timer def request_cache(ttl: float): diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index 60cc0a026..07e08ea9a 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -9,6 +9,7 @@ from werkzeug.exceptions import NotFound from common import DIContainer from monkey_island.cc.database import database, mongo +from monkey_island.cc.resources import RemoteRun from monkey_island.cc.resources.agent_controls import StopAgentCheck, StopAllAgents from monkey_island.cc.resources.attack.attack_report import AttackReport from monkey_island.cc.resources.auth.auth import Authenticate, init_jwt @@ -38,7 +39,6 @@ from monkey_island.cc.resources.pba_file_download import PBAFileDownload from monkey_island.cc.resources.pba_file_upload import FileUpload from monkey_island.cc.resources.propagation_credentials import PropagationCredentials from monkey_island.cc.resources.ransomware_report import RansomwareReport -from monkey_island.cc.resources.remote_run import RemoteRun from monkey_island.cc.resources.root import Root from monkey_island.cc.resources.security_report import SecurityReport from monkey_island.cc.resources.telemetry import Telemetry diff --git a/monkey/monkey_island/cc/resources/__init__.py b/monkey/monkey_island/cc/resources/__init__.py index e69de29bb..b5bbb1108 100644 --- a/monkey/monkey_island/cc/resources/__init__.py +++ b/monkey/monkey_island/cc/resources/__init__.py @@ -0,0 +1 @@ +from .remote_run import RemoteRun diff --git a/monkey/monkey_island/cc/resources/remote_run.py b/monkey/monkey_island/cc/resources/remote_run.py index f918c9253..8bb0752aa 100644 --- a/monkey/monkey_island/cc/resources/remote_run.py +++ b/monkey/monkey_island/cc/resources/remote_run.py @@ -1,12 +1,13 @@ import json +from typing import Sequence import flask_restful from botocore.exceptions import ClientError, NoCredentialsError from flask import jsonify, make_response, request from monkey_island.cc.resources.auth.auth import jwt_required -from monkey_island.cc.services import aws_service -from monkey_island.cc.services.remote_run_aws import RemoteRunAwsService +from monkey_island.cc.services import AWSService +from monkey_island.cc.services.aws import AWSCommandResults CLIENT_ERROR_FORMAT = ( "ClientError, error message: '{}'. Probably, the IAM role that has been associated with the " @@ -19,20 +20,18 @@ NO_CREDS_ERROR_FORMAT = ( class RemoteRun(flask_restful.Resource): - 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) + def __init__(self, aws_service: AWSService): + self._aws_service = aws_service @jwt_required def get(self): action = request.args.get("action") if action == "list_aws": - is_aws = aws_service.is_on_aws() + is_aws = self._aws_service.island_is_running_on_aws() resp = {"is_aws": is_aws} if is_aws: try: - resp["instances"] = aws_service.get_instances() + resp["instances"] = self._aws_service.get_managed_instances() except NoCredentialsError as e: resp["error"] = NO_CREDS_ERROR_FORMAT.format(e) return jsonify(resp) @@ -46,11 +45,28 @@ class RemoteRun(flask_restful.Resource): @jwt_required def post(self): body = json.loads(request.data) - resp = {} if body.get("type") == "aws": - result = self.run_aws_monkeys(body) - resp["result"] = result - return jsonify(resp) + results = self.run_aws_monkeys(body) + return RemoteRun._encode_results(results) # default action return make_response({"error": "Invalid action"}, 500) + + def run_aws_monkeys(self, request_body) -> Sequence[AWSCommandResults]: + instances = request_body.get("instances") + island_ip = request_body.get("island_ip") + + return self._aws_service.run_agents_on_managed_instances(instances, island_ip) + + @staticmethod + def _encode_results(results: Sequence[AWSCommandResults]): + result = list(map(RemoteRun._aws_command_results_to_encodable_dict, results)) + response = {"result": result} + + return jsonify(response) + + @staticmethod + def _aws_command_results_to_encodable_dict(aws_command_results: AWSCommandResults): + res_dict = aws_command_results.__dict__ + res_dict["status"] = res_dict["status"].name.lower() + return res_dict diff --git a/monkey/monkey_island/cc/server_setup.py b/monkey/monkey_island/cc/server_setup.py index c883d70db..8b1e56015 100644 --- a/monkey/monkey_island/cc/server_setup.py +++ b/monkey/monkey_island/cc/server_setup.py @@ -3,7 +3,6 @@ import json import logging import sys from pathlib import Path -from threading import Thread import gevent.hub from gevent.pywsgi import WSGIServer @@ -29,7 +28,6 @@ from monkey_island.cc.server_utils.consts import ( # noqa: E402 ) from monkey_island.cc.server_utils.island_logger import reset_logger, setup_logging # noqa: E402 from monkey_island.cc.services.initialize import initialize_services # noqa: E402 -from monkey_island.cc.services.reporting.exporter_init import populate_exporter_list # noqa: E402 from monkey_island.cc.services.utils.network_utils import local_ip_addresses # noqa: E402 from monkey_island.cc.setup import island_config_options_validator # noqa: E402 from monkey_island.cc.setup.data_dir import IncompatibleDataDirectory, setup_data_dir # noqa: E402 @@ -132,8 +130,6 @@ def _configure_gevent_exception_handling(data_dir): def _start_island_server( should_setup_only: bool, config_options: IslandConfigOptions, container: DIContainer ): - # AWS exporter takes a long time to load - Thread(target=populate_exporter_list, name="Report exporter list", daemon=True).start() app = init_app(mongo_setup.MONGO_URL, container) if should_setup_only: diff --git a/monkey/monkey_island/cc/server_utils/aws_cmd_result.py b/monkey/monkey_island/cc/server_utils/aws_cmd_result.py deleted file mode 100644 index 0a6c5f3cc..000000000 --- a/monkey/monkey_island/cc/server_utils/aws_cmd_result.py +++ /dev/null @@ -1,29 +0,0 @@ -from common.cmd.cmd_result import CmdResult - - -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["ResponseCode"], - command_info["StandardOutputContent"], - command_info["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["Status"] == "Success") or ( - is_timeout and (command_info["Status"] == "InProgress") - ) diff --git a/monkey/monkey_island/cc/server_utils/aws_cmd_runner.py b/monkey/monkey_island/cc/server_utils/aws_cmd_runner.py deleted file mode 100644 index 16c959197..000000000 --- a/monkey/monkey_island/cc/server_utils/aws_cmd_runner.py +++ /dev/null @@ -1,45 +0,0 @@ -import logging -import time - -from common.cmd.cmd_runner import CmdRunner -from common.cmd.cmd_status import CmdStatus -from monkey_island.cc.server_utils.aws_cmd_result import AwsCmdResult -from monkey_island.cc.services import aws_service - -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 = aws_service.get_client("ssm", region) - - def query_command(self, command_id): - time.sleep(2) - 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["Status"] == "InProgress": - return CmdStatus.IN_PROGRESS - elif command_info["Status"] == "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"] diff --git a/monkey/monkey_island/cc/services/__init__.py b/monkey/monkey_island/cc/services/__init__.py index 43aa39382..f50990ce3 100644 --- a/monkey/monkey_island/cc/services/__init__.py +++ b/monkey/monkey_island/cc/services/__init__.py @@ -3,3 +3,9 @@ from .directory_file_storage_service import DirectoryFileStorageService from .authentication.authentication_service import AuthenticationService from .authentication.json_file_user_datastore import JsonFileUserDatastore + +from .aws import AWSService + +# TODO: This is a temporary import to keep some tests passing. Remove it before merging #1928 to +# develop. +from .aws import aws_service diff --git a/monkey/monkey_island/cc/services/aws/__init__.py b/monkey/monkey_island/cc/services/aws/__init__.py new file mode 100644 index 000000000..78ed07bc5 --- /dev/null +++ b/monkey/monkey_island/cc/services/aws/__init__.py @@ -0,0 +1,2 @@ +from .aws_service import AWSService +from .aws_command_runner import AWSCommandResults, AWSCommandStatus diff --git a/monkey/monkey_island/cc/services/aws/aws_command_runner.py b/monkey/monkey_island/cc/services/aws/aws_command_runner.py new file mode 100644 index 000000000..e896e2cfb --- /dev/null +++ b/monkey/monkey_island/cc/services/aws/aws_command_runner.py @@ -0,0 +1,149 @@ +import logging +import time +from dataclasses import dataclass +from enum import Enum, auto + +import botocore + +from common.utils import Timer + +STATUS_CHECK_SLEEP_TIME = 1 +LINUX_DOCUMENT_NAME = "AWS-RunShellScript" +WINDOWS_DOCUMENT_NAME = "AWS-RunPowerShellScript" + +logger = logging.getLogger(__name__) + + +class AWSCommandStatus(Enum): + SUCCESS = auto() + IN_PROGRESS = auto() + ERROR = auto() + + +@dataclass(frozen=True) +class AWSCommandResults: + instance_id: str + response_code: int + stdout: str + stderr: str + status: AWSCommandStatus + + @property + def success(self): + return self.status == AWSCommandStatus.SUCCESS + + +def start_infection_monkey_agent( + aws_client: botocore.client.BaseClient, + target_instance_id: str, + target_os: str, + island_ip: str, + timeout: float, +) -> AWSCommandResults: + """ + Run a command on a remote AWS instance + """ + command = _get_run_agent_command(target_os, island_ip) + command_id = _run_command_async(aws_client, target_instance_id, target_os, command) + + _wait_for_command_to_complete(aws_client, target_instance_id, command_id, timeout) + return _fetch_command_results(aws_client, target_instance_id, command_id) + + +def _get_run_agent_command(target_os: str, island_ip: str): + if target_os == "linux": + return _get_run_monkey_cmd_linux_line(island_ip) + + return _get_run_monkey_cmd_windows_line(island_ip) + + +def _get_run_monkey_cmd_linux_line(island_ip): + binary_name = "monkey-linux-64" + + download_url = f"https://{island_ip}:5000/api/agent/download/linux" + download_cmd = f"wget --no-check-certificate {download_url} -O {binary_name}" + + chmod_cmd = f"chmod +x {binary_name}" + run_agent_cmd = f"./{binary_name} m0nk3y -s {island_ip}:5000" + + return f"{download_cmd}; {chmod_cmd}; {run_agent_cmd}" + + +def _get_run_monkey_cmd_windows_line(island_ip): + agent_exe_path = r".\\monkey.exe" + + ignore_ssl_errors_cmd = ( + "[System.Net.ServicePointManager]::ServerCertificateValidationCallback = {$true}" + ) + + download_url = f"https://{island_ip}:5000/api/agent/download/windows" + download_cmd = ( + f"(New-Object System.Net.WebClient).DownloadFile('{download_url}', '{agent_exe_path}')" + ) + + run_agent_cmd = ( + f"Start-Process -FilePath '{agent_exe_path}' -ArgumentList 'm0nk3y -s {island_ip}:5000'" + ) + + return f"{ignore_ssl_errors_cmd}; {download_cmd}; {run_agent_cmd};" + + +def _run_command_async( + aws_client: botocore.client.BaseClient, target_instance_id: str, target_os: str, command: str +): + doc_name = LINUX_DOCUMENT_NAME if target_os == "linux" else WINDOWS_DOCUMENT_NAME + + logger.debug(f'Running command on {target_instance_id} -- {doc_name}: "{command}"') + command_response = aws_client.send_command( + DocumentName=doc_name, + Parameters={"commands": [command]}, + InstanceIds=[target_instance_id], + ) + + command_id = command_response["Command"]["CommandId"] + logger.debug( + f"Started command on AWS instance {target_instance_id} with command ID {command_id}" + ) + + return command_id + + +def _wait_for_command_to_complete( + aws_client: botocore.client.BaseClient, target_instance_id: str, command_id: str, timeout: float +): + timer = Timer() + timer.set(timeout) + + while not timer.is_expired(): + time.sleep(STATUS_CHECK_SLEEP_TIME) + + command_results = _fetch_command_results(aws_client, target_instance_id, command_id) + logger.debug(f"Command {command_id} status: {command_results.status.name}") + + if command_results.status != AWSCommandStatus.IN_PROGRESS: + return + + +def _fetch_command_results( + aws_client: botocore.client.BaseClient, target_instance_id: str, command_id: str +) -> AWSCommandResults: + command_results = aws_client.get_command_invocation( + CommandId=command_id, InstanceId=target_instance_id + ) + command_status = command_results["Status"] + logger.debug(f"Command {command_id} status: {command_status}") + + if command_status == "Success": + aws_command_result_status = AWSCommandStatus.SUCCESS + elif command_status == "InProgress": + aws_command_result_status = AWSCommandStatus.IN_PROGRESS + else: + aws_command_result_status = AWSCommandStatus.ERROR + + return AWSCommandResults( + target_instance_id, + command_results["ResponseCode"], + command_results["StandardOutputContent"], + command_results["StandardErrorContent"], + aws_command_result_status, + ) diff --git a/monkey/monkey_island/cc/services/aws/aws_service.py b/monkey/monkey_island/cc/services/aws/aws_service.py new file mode 100644 index 000000000..92a0e8bf5 --- /dev/null +++ b/monkey/monkey_island/cc/services/aws/aws_service.py @@ -0,0 +1,135 @@ +import logging +from queue import Queue +from threading import Thread +from typing import Any, Iterable, Mapping, Sequence + +import boto3 +import botocore + +from common.aws.aws_instance import AWSInstance +from common.utils.code_utils import queue_to_list + +from .aws_command_runner import AWSCommandResults, start_infection_monkey_agent + +DEFAULT_REMOTE_COMMAND_TIMEOUT = 5 +INSTANCE_INFORMATION_LIST_KEY = "InstanceInformationList" +INSTANCE_ID_KEY = "InstanceId" +COMPUTER_NAME_KEY = "ComputerName" +PLATFORM_TYPE_KEY = "PlatformType" +IP_ADDRESS_KEY = "IPAddress" + +logger = logging.getLogger(__name__) + + +class AWSService: + def __init__(self, aws_instance: AWSInstance): + """ + :param aws_instance: An AWSInstance object representing the AWS instance that the Island is + running on + """ + self._aws_instance = aws_instance + + def island_is_running_on_aws(self) -> bool: + """ + :return: True if the island is running on an AWS instance. False otherwise. + :rtype: bool + """ + return self._aws_instance.is_instance + + @property + def island_aws_instance(self) -> AWSInstance: + """ + :return: an AWSInstance object representing the AWS instance that the Island is running on. + :rtype: AWSInstance + """ + return self._aws_instance + + def get_managed_instances(self) -> Sequence[Mapping[str, str]]: + """ + :return: A sequence of mappings, where each Mapping represents a managed AWS instance that + is accessible from the Island. + :rtype: Sequence[Mapping[str, str]] + """ + raw_managed_instances_info = self._get_raw_managed_instances() + return _filter_relevant_instance_info(raw_managed_instances_info) + + def _get_raw_managed_instances(self) -> Sequence[Mapping[str, Any]]: + """ + Get the information for all instances with the relevant roles. + + This function will assume that it's running on an EC2 instance with the correct IAM role. + See https://boto3.amazonaws.com/v1/documentation/api/latest/guide/configuration.html#iam + -role for details. + + :raises: botocore.exceptions.ClientError if can't describe local instance information. + :return: All visible instances from this instance + """ + ssm_client = boto3.client("ssm", self.island_aws_instance.region) + try: + response = ssm_client.describe_instance_information() + return response[INSTANCE_INFORMATION_LIST_KEY] + except botocore.exceptions.ClientError as err: + logger.warning("AWS client error while trying to get manage dinstances: {err}") + raise err + + def run_agents_on_managed_instances( + self, + instances: Iterable[Mapping[str, str]], + island_ip: str, + timeout: float = DEFAULT_REMOTE_COMMAND_TIMEOUT, + ) -> Sequence[AWSCommandResults]: + """ + Run an agent on one or more managed AWS instances. + :param instances: An iterable of instances that the agent will be run on + :param island_ip: The IP address of the Island to pass to the new agents + :param timeout: The maximum number of seconds to wait for the agents to start + :return: A sequence of AWSCommandResults + """ + + results_queue = Queue() + command_threads = [] + for i in instances: + t = Thread( + target=self._run_agent_on_managed_instance, + args=(results_queue, i["instance_id"], i["os"], island_ip, timeout), + daemon=True, + ) + t.start() + command_threads.append(t) + + for thread in command_threads: + thread.join() + + return queue_to_list(results_queue) + + def _run_agent_on_managed_instance( + self, results_queue: Queue, instance_id: str, os: str, island_ip: str, timeout: float + ): + ssm_client = boto3.client("ssm", self.island_aws_instance.region) + command_results = start_infection_monkey_agent( + ssm_client, instance_id, os, island_ip, timeout + ) + results_queue.put(command_results) + + +def _filter_relevant_instance_info(raw_managed_instances_info: Sequence[Mapping[str, Any]]): + """ + Consume raw instance data from the AWS API and return only those fields that are relevant for + Infection Monkey. + + :param raw_managed_instances_info: The output of + DescribeInstanceInformation["InstanceInformation"] from the + AWS API + :return: A sequence of mappings, where each Mapping represents a managed AWS instance that + is accessible from the Island. + :rtype: Sequence[Mapping[str, str]] + """ + return [ + { + "instance_id": managed_instance[INSTANCE_ID_KEY], + "name": managed_instance[COMPUTER_NAME_KEY], + "os": managed_instance[PLATFORM_TYPE_KEY].lower(), + "ip_address": managed_instance[IP_ADDRESS_KEY], + } + for managed_instance in raw_managed_instances_info + ] diff --git a/monkey/monkey_island/cc/services/aws_service.py b/monkey/monkey_island/cc/services/aws_service.py deleted file mode 100644 index b0f252608..000000000 --- a/monkey/monkey_island/cc/services/aws_service.py +++ /dev/null @@ -1,99 +0,0 @@ -import logging -from functools import wraps -from threading import Event -from typing import Callable, Optional - -import boto3 -import botocore - -from common.aws.aws_instance import AwsInstance - -INSTANCE_INFORMATION_LIST_KEY = "InstanceInformationList" -INSTANCE_ID_KEY = "InstanceId" -COMPUTER_NAME_KEY = "ComputerName" -PLATFORM_TYPE_KEY = "PlatformType" -IP_ADDRESS_KEY = "IPAddress" - -logger = logging.getLogger(__name__) - - -def filter_instance_data_from_aws_response(response): - return [ - { - "instance_id": x[INSTANCE_ID_KEY], - "name": x[COMPUTER_NAME_KEY], - "os": x[PLATFORM_TYPE_KEY].lower(), - "ip_address": x[IP_ADDRESS_KEY], - } - for x in response[INSTANCE_INFORMATION_LIST_KEY] - ] - - -aws_instance: Optional[AwsInstance] = None -AWS_INFO_FETCH_TIMEOUT = 10.0 # Seconds -init_done = Event() - - -def initialize(): - global aws_instance - aws_instance = AwsInstance() - init_done.set() - - -def wait_init_done(fnc: Callable): - @wraps(fnc) - def inner(): - awaited = init_done.wait(AWS_INFO_FETCH_TIMEOUT) - if not awaited: - logger.error( - f"AWS service couldn't initialize in time! " - f"Current timeout is {AWS_INFO_FETCH_TIMEOUT}, " - f"but AWS info took longer to fetch from metadata server." - ) - return - fnc() - - return inner - - -@wait_init_done -def is_on_aws(): - return aws_instance.is_instance - - -@wait_init_done -def get_region(): - return aws_instance.region - - -@wait_init_done -def get_account_id(): - return aws_instance.account_id - - -@wait_init_done -def get_client(client_type): - return boto3.client(client_type, region_name=aws_instance.region) - - -@wait_init_done -def get_instances(): - """ - Get the information for all instances with the relevant roles. - - This function will assume that it's running on an EC2 instance with the correct IAM role. - See https://boto3.amazonaws.com/v1/documentation/api/latest/guide/configuration.html#iam - -role for details. - - :raises: botocore.exceptions.ClientError if can't describe local instance information. - :return: All visible instances from this instance - """ - local_ssm_client = boto3.client("ssm", aws_instance.region) - try: - response = local_ssm_client.describe_instance_information() - - filtered_instances_data = filter_instance_data_from_aws_response(response) - return filtered_instances_data - except botocore.exceptions.ClientError as e: - logger.warning("AWS client error while trying to get instances: " + e) - raise e diff --git a/monkey/monkey_island/cc/services/initialize.py b/monkey/monkey_island/cc/services/initialize.py index faa3bcef9..78846cff5 100644 --- a/monkey/monkey_island/cc/services/initialize.py +++ b/monkey/monkey_island/cc/services/initialize.py @@ -1,26 +1,28 @@ from pathlib import Path -from threading import Thread from common import DIContainer -from monkey_island.cc.services import DirectoryFileStorageService, IFileStorageService, aws_service +from common.aws import AWSInstance +from monkey_island.cc.services import AWSService, DirectoryFileStorageService, IFileStorageService from monkey_island.cc.services.post_breach_files import PostBreachFilesService from monkey_island.cc.services.run_local_monkey import LocalMonkeyRunService from . import AuthenticationService, JsonFileUserDatastore +from .reporting.report import ReportService def initialize_services(data_dir: Path) -> DIContainer: container = DIContainer() + container.register_instance(AWSInstance, AWSInstance()) + container.register_instance( IFileStorageService, DirectoryFileStorageService(data_dir / "custom_pbas") ) - - # Takes a while so it's best to start it in the background - Thread(target=aws_service.initialize, name="AwsService initialization", daemon=True).start() + container.register_instance(AWSService, container.resolve(AWSService)) # This is temporary until we get DI all worked out. PostBreachFilesService.initialize(container.resolve(IFileStorageService)) LocalMonkeyRunService.initialize(data_dir) AuthenticationService.initialize(data_dir, JsonFileUserDatastore(data_dir)) + ReportService.initialize(container.resolve(AWSService)) return container diff --git a/monkey/monkey_island/cc/services/remote_run_aws.py b/monkey/monkey_island/cc/services/remote_run_aws.py deleted file mode 100644 index c3219171c..000000000 --- a/monkey/monkey_island/cc/services/remote_run_aws.py +++ /dev/null @@ -1,82 +0,0 @@ -import logging - -from common.cmd.cmd import Cmd -from common.cmd.cmd_runner import CmdRunner -from monkey_island.cc.server_utils.aws_cmd_runner import AwsCmdRunner - -logger = logging.getLogger(__name__) - - -class RemoteRunAwsService: - @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 - """ - return CmdRunner.run_multiple_commands( - instances, - lambda instance: RemoteRunAwsService._run_aws_monkey_cmd_async( - instance["instance_id"], - RemoteRunAwsService._is_linux(instance["os"]), - island_ip, - ), - lambda _, result: result.is_success, - ) - - @staticmethod - def _run_aws_monkey_cmd_async(instance_id, is_linux, island_ip): - """ - 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 - :return: Cmd - """ - cmd_text = RemoteRunAwsService._get_run_monkey_cmd_line(is_linux, 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(island_ip): - return ( - r"wget --no-check-certificate https://" - + island_ip - + r":5000/api/agent/download/linux " - + r"-O monkey-linux-64" - + r"; chmod +x monkey-linux-64" - + r"; ./monkey-linux-64" - + r" m0nk3y -s " - + island_ip - + r":5000" - ) - - @staticmethod - def _get_run_monkey_cmd_windows_line(island_ip): - return ( - r"[System.Net.ServicePointManager]::ServerCertificateValidationCallback = {" - r"$true}; (New-Object System.Net.WebClient).DownloadFile('https://" - + island_ip - + r":5000/api/agent/download/windows'" - + r"'.\\monkey.exe'); " - r";Start-Process -FilePath '.\\monkey.exe' " - r"-ArgumentList 'm0nk3y -s " + island_ip + r":5000'; " - ) - - @staticmethod - def _get_run_monkey_cmd_line(is_linux, island_ip): - return ( - RemoteRunAwsService._get_run_monkey_cmd_linux_line(island_ip) - if is_linux - else RemoteRunAwsService._get_run_monkey_cmd_windows_line(island_ip) - ) diff --git a/monkey/monkey_island/cc/services/reporting/aws_exporter.py b/monkey/monkey_island/cc/services/reporting/aws_exporter.py index aeb76b4d5..95e4c80f0 100644 --- a/monkey/monkey_island/cc/services/reporting/aws_exporter.py +++ b/monkey/monkey_island/cc/services/reporting/aws_exporter.py @@ -1,21 +1,15 @@ import logging import uuid from datetime import datetime +from typing import Mapping import boto3 from botocore.exceptions import UnknownServiceError -from monkey_island.cc.services import aws_service -from monkey_island.cc.services.reporting.exporter import Exporter - -__authors__ = ["maor.rayzin", "shay.nehmad"] - - +from common.aws import AWSInstance from monkey_island.cc.services.reporting.issue_processing.exploit_processing.exploiter_descriptor_enum import ( # noqa:E501 (Long import) ExploiterDescriptorEnum, ) - -# noqa:E501 (Long import) from monkey_island.cc.services.reporting.issue_processing.exploit_processing.exploiter_report_info import ( # noqa:E501 (Long import) CredentialType, ) @@ -25,376 +19,351 @@ logger = logging.getLogger(__name__) INFECTION_MONKEY_ARN = "324264561773:product/guardicore/aws-infection-monkey" -class AWSExporter(Exporter): - @staticmethod - def handle_report(report_json): - - findings_list = [] - issues_list = report_json["recommendations"]["issues"] - if not issues_list: - logger.info("No issues were found by the monkey, no need to send anything") - return True - - current_aws_region = aws_service.get_region() - - for machine in issues_list: - for issue in issues_list[machine]: - try: - if "aws_instance_id" in issue: - findings_list.append( - AWSExporter._prepare_finding(issue, current_aws_region) - ) - except AWSExporter.FindingNotFoundError as e: - logger.error(e) - - if not AWSExporter._send_findings(findings_list, current_aws_region): - logger.error("Exporting findings to aws failed") - return False - +def handle_report(report_json: Mapping, aws_instance: AWSInstance): + findings_list = [] + issues_list = report_json["recommendations"]["issues"] + if not issues_list: + logger.info("No issues were found by the monkey, no need to send anything") return True - @staticmethod - def merge_two_dicts(x, y): - z = x.copy() # start with x's keys and values - z.update(y) # modifies z with y's keys and values & returns None - return z + for machine in issues_list: + for issue in issues_list[machine]: + try: + if "aws_instance_id" in issue: + findings_list.append(_prepare_finding(issue, aws_instance)) + except FindingNotFoundError as e: + logger.error(e) - @staticmethod - def _prepare_finding(issue, region): - findings_dict = { - "island_cross_segment": AWSExporter._handle_island_cross_segment_issue, - ExploiterDescriptorEnum.SSH.value.class_name: { - CredentialType.PASSWORD.value: AWSExporter._handle_ssh_issue, - CredentialType.KEY.value: AWSExporter._handle_ssh_key_issue, - }, - "tunnel": AWSExporter._handle_tunnel_issue, - ExploiterDescriptorEnum.SMB.value.class_name: { - CredentialType.PASSWORD.value: AWSExporter._handle_smb_password_issue, - CredentialType.HASH.value: AWSExporter._handle_smb_pth_issue, - }, - "shared_passwords": AWSExporter._handle_shared_passwords_issue, - ExploiterDescriptorEnum.WMI.value.class_name: { - CredentialType.PASSWORD.value: AWSExporter._handle_wmi_password_issue, - CredentialType.HASH.value: AWSExporter._handle_wmi_pth_issue, - }, - "shared_passwords_domain": AWSExporter._handle_shared_passwords_domain_issue, - "shared_admins_domain": AWSExporter._handle_shared_admins_domain_issue, - "strong_users_on_crit": AWSExporter._handle_strong_users_on_crit_issue, - ExploiterDescriptorEnum.HADOOP.value.class_name: AWSExporter._handle_hadoop_issue, - } + if not _send_findings(findings_list, aws_instance.region): + logger.error("Exporting findings to aws failed") + return False - configured_product_arn = INFECTION_MONKEY_ARN - product_arn = "arn:aws:securityhub:{region}:{arn}".format( - region=region, arn=configured_product_arn - ) - instance_arn = "arn:aws:ec2:" + str(region) + ":instance:{instance_id}" - # Not suppressing error here on purpose. - account_id = aws_service.get_account_id() - logger.debug("aws account id acquired: {}".format(account_id)) + return True - aws_finding = { - "SchemaVersion": "2018-10-08", - "Id": uuid.uuid4().hex, - "ProductArn": product_arn, - "GeneratorId": issue["type"], - "AwsAccountId": account_id, - "RecordState": "ACTIVE", - "Types": ["Software and Configuration Checks/Vulnerabilities/CVE"], - "CreatedAt": datetime.now().isoformat() + "Z", - "UpdatedAt": datetime.now().isoformat() + "Z", - } - processor = AWSExporter._get_issue_processor(findings_dict, issue) +def merge_two_dicts(x, y): + z = x.copy() # start with x's keys and values + z.update(y) # modifies z with y's keys and values & returns None + return z - return AWSExporter.merge_two_dicts(aws_finding, processor(issue, instance_arn)) - @staticmethod - def _get_issue_processor(finding_dict, issue): - try: - processor = finding_dict[issue["type"]] - if type(processor) == dict: - processor = processor[issue["credential_type"]] - return processor - except KeyError: - raise AWSExporter.FindingNotFoundError( - f"Finding {issue['type']} not added as AWS exportable finding" - ) +def _prepare_finding(issue, aws_instance: AWSInstance): + findings_dict = { + "island_cross_segment": _handle_island_cross_segment_issue, + ExploiterDescriptorEnum.SSH.value.class_name: { + CredentialType.PASSWORD.value: _handle_ssh_issue, + CredentialType.KEY.value: _handle_ssh_key_issue, + }, + "tunnel": _handle_tunnel_issue, + ExploiterDescriptorEnum.SMB.value.class_name: { + CredentialType.PASSWORD.value: _handle_smb_password_issue, + CredentialType.HASH.value: _handle_smb_pth_issue, + }, + "shared_passwords": _handle_shared_passwords_issue, + ExploiterDescriptorEnum.WMI.value.class_name: { + CredentialType.PASSWORD.value: _handle_wmi_password_issue, + CredentialType.HASH.value: _handle_wmi_pth_issue, + }, + "shared_passwords_domain": _handle_shared_passwords_domain_issue, + "shared_admins_domain": _handle_shared_admins_domain_issue, + "strong_users_on_crit": _handle_strong_users_on_crit_issue, + ExploiterDescriptorEnum.HADOOP.value.class_name: _handle_hadoop_issue, + } - class FindingNotFoundError(Exception): - pass + configured_product_arn = INFECTION_MONKEY_ARN + product_arn = "arn:aws:securityhub:{region}:{arn}".format( + region=aws_instance.region, arn=configured_product_arn + ) + instance_arn = "arn:aws:ec2:" + str(aws_instance.region) + ":instance:{instance_id}" + account_id = aws_instance.account_id + logger.debug("aws account id acquired: {}".format(account_id)) - @staticmethod - def _send_findings(findings_list, region): - try: - logger.debug("Trying to acquire securityhub boto3 client in " + region) - security_hub_client = boto3.client("securityhub", region_name=region) - logger.debug("Client acquired: {0}".format(repr(security_hub_client))) + aws_finding = { + "SchemaVersion": "2018-10-08", + "Id": uuid.uuid4().hex, + "ProductArn": product_arn, + "GeneratorId": issue["type"], + "AwsAccountId": account_id, + "RecordState": "ACTIVE", + "Types": ["Software and Configuration Checks/Vulnerabilities/CVE"], + "CreatedAt": datetime.now().isoformat() + "Z", + "UpdatedAt": datetime.now().isoformat() + "Z", + } - # Assumes the machine has the correct IAM role to do this, @see - # https://github.com/guardicore/monkey/wiki/Monkey-Island:-Running-the-monkey-on-AWS - # -EC2-instances - import_response = security_hub_client.batch_import_findings(Findings=findings_list) - logger.debug("Import findings response: {0}".format(repr(import_response))) + processor = _get_issue_processor(findings_dict, issue) - if import_response["ResponseMetadata"]["HTTPStatusCode"] == 200: - return True - else: - return False - except UnknownServiceError as e: - logger.warning( - "AWS exporter called but AWS-CLI security hub service is not installed. " - "Error: {}".format(e) - ) - return False - except Exception as e: - logger.exception("AWS security hub findings failed to send. Error: {}".format(e)) - return False + return merge_two_dicts(aws_finding, processor(issue, instance_arn)) - @staticmethod - def _get_finding_resource(instance_id, instance_arn): - if instance_id: - return [{"Type": "AwsEc2Instance", "Id": instance_arn.format(instance_id=instance_id)}] + +def _get_issue_processor(finding_dict, issue): + try: + processor = finding_dict[issue["type"]] + if type(processor) == dict: + processor = processor[issue["credential_type"]] + return processor + except KeyError: + raise FindingNotFoundError(f"Finding {issue['type']} not added as AWS exportable finding") + + +class FindingNotFoundError(Exception): + pass + + +def _send_findings(findings_list, region): + try: + logger.debug("Trying to acquire securityhub boto3 client in " + region) + security_hub_client = boto3.client("securityhub", region_name=region) + logger.debug("Client acquired: {0}".format(repr(security_hub_client))) + + # Assumes the machine has the correct IAM role to do this, @see + # https://github.com/guardicore/monkey/wiki/Monkey-Island:-Running-the-monkey-on-AWS + # -EC2-instances + import_response = security_hub_client.batch_import_findings(Findings=findings_list) + logger.debug("Import findings response: {0}".format(repr(import_response))) + + if import_response["ResponseMetadata"]["HTTPStatusCode"] == 200: + return True else: - return [{"Type": "Other", "Id": "None"}] - - @staticmethod - def _build_generic_finding( - severity, title, description, recommendation, instance_arn, instance_id=None - ): - finding = { - "Severity": {"Product": severity, "Normalized": 100}, - "Resources": AWSExporter._get_finding_resource(instance_id, instance_arn), - "Title": title, - "Description": description, - "Remediation": {"Recommendation": {"Text": recommendation}}, - } - - return finding - - @staticmethod - def _handle_tunnel_issue(issue, instance_arn): - - return AWSExporter._build_generic_finding( - severity=5, - title="Weak segmentation - Machines were able to communicate over unused ports.", - description="Use micro-segmentation policies to disable communication other than " - "the required.", - recommendation="Machines are not locked down at port level. " - "Network tunnel was set up from {0} to {1}".format(issue["machine"], issue["dest"]), - instance_arn=instance_arn, - instance_id=issue["aws_instance_id"] if "aws_instance_id" in issue else None, + return False + except UnknownServiceError as e: + logger.warning( + "AWS exporter called but AWS-CLI security hub service is not installed. " + "Error: {}".format(e) ) + return False + except Exception as e: + logger.exception("AWS security hub findings failed to send. Error: {}".format(e)) + return False - @staticmethod - def _handle_smb_pth_issue(issue, instance_arn): - return AWSExporter._build_generic_finding( - severity=5, - title="Machines are accessible using passwords supplied by the user during the " - "Monkey's configuration.", - description="Change {0}'s password to a complex one-use password that is not " - "shared with other computers on the " - "network.".format(issue["username"]), - recommendation="The machine {0}({1}) is vulnerable to a SMB attack. The Monkey " - "used a pass-the-hash attack over " - "SMB protocol with user {2}.".format( - issue["machine"], issue["ip_address"], issue["username"] - ), - instance_arn=instance_arn, - instance_id=issue["aws_instance_id"] if "aws_instance_id" in issue else None, - ) +def _get_finding_resource(instance_id, instance_arn): + if instance_id: + return [{"Type": "AwsEc2Instance", "Id": instance_arn.format(instance_id=instance_id)}] + else: + return [{"Type": "Other", "Id": "None"}] - @staticmethod - def _handle_ssh_issue(issue, instance_arn): - return AWSExporter._build_generic_finding( - severity=1, - title="Machines are accessible using SSH passwords supplied by the user during " - "the Monkey's configuration.", - description="Change {0}'s password to a complex one-use password that is not " - "shared with other computers on the " - "network.".format(issue["username"]), - recommendation="The machine {0} ({1}) is vulnerable to a SSH attack. The Monkey " - "authenticated over the SSH" - " protocol with user {2} and its " - "password.".format(issue["machine"], issue["ip_address"], issue["username"]), - instance_arn=instance_arn, - instance_id=issue["aws_instance_id"] if "aws_instance_id" in issue else None, - ) +def _build_generic_finding( + severity, title, description, recommendation, instance_arn, instance_id=None +): + finding = { + "Severity": {"Product": severity, "Normalized": 100}, + "Resources": _get_finding_resource(instance_id, instance_arn), + "Title": title, + "Description": description, + "Remediation": {"Recommendation": {"Text": recommendation}}, + } - @staticmethod - def _handle_ssh_key_issue(issue, instance_arn): + return finding - return AWSExporter._build_generic_finding( - severity=1, - title="Machines are accessible using SSH passwords supplied by the user during " - "the Monkey's configuration.", - description="Protect {ssh_key} private key with a pass phrase.".format( - ssh_key=issue["ssh_key"] - ), - recommendation="The machine {machine} ({ip_address}) is vulnerable to a SSH " - "attack. The Monkey authenticated " - "over the SSH protocol with private key {ssh_key}.".format( - machine=issue["machine"], ip_address=issue["ip_address"], ssh_key=issue["ssh_key"] - ), - instance_arn=instance_arn, - instance_id=issue["aws_instance_id"] if "aws_instance_id" in issue else None, - ) - @staticmethod - def _handle_island_cross_segment_issue(issue, instance_arn): +def _handle_tunnel_issue(issue, instance_arn): + return _build_generic_finding( + severity=5, + title="Weak segmentation - Machines were able to communicate over unused ports.", + description="Use micro-segmentation policies to disable communication other than " + "the required.", + recommendation="Machines are not locked down at port level. " + "Network tunnel was set up from {0} to {1}".format(issue["machine"], issue["dest"]), + instance_arn=instance_arn, + instance_id=issue["aws_instance_id"] if "aws_instance_id" in issue else None, + ) - return AWSExporter._build_generic_finding( - severity=1, - title="Weak segmentation - Machines from different segments are able to " - "communicate.", - description="Segment your network and make sure there is no communication between " - "machines from different " - "segments.", - recommendation="The network can probably be segmented. A monkey instance on \ - {0} in the networks {1} \ - could directly access the Monkey Island server in the networks {2}.".format( - issue["machine"], issue["networks"], issue["server_networks"] - ), - instance_arn=instance_arn, - instance_id=issue["aws_instance_id"] if "aws_instance_id" in issue else None, - ) - @staticmethod - def _handle_shared_passwords_issue(issue, instance_arn): +def _handle_smb_pth_issue(issue, instance_arn): + return _build_generic_finding( + severity=5, + title="Machines are accessible using passwords supplied by the user during the " + "Monkey's configuration.", + description="Change {0}'s password to a complex one-use password that is not " + "shared with other computers on the " + "network.".format(issue["username"]), + recommendation="The machine {0}({1}) is vulnerable to a SMB attack. The Monkey " + "used a pass-the-hash attack over " + "SMB protocol with user {2}.".format( + issue["machine"], issue["ip_address"], issue["username"] + ), + instance_arn=instance_arn, + instance_id=issue["aws_instance_id"] if "aws_instance_id" in issue else None, + ) - return AWSExporter._build_generic_finding( - severity=1, - title="Multiple users have the same password", - description="Some users are sharing passwords, this should be fixed by changing " - "passwords.", - recommendation="These users are sharing access password: {0}.".format( - issue["shared_with"] - ), - instance_arn=instance_arn, - instance_id=issue["aws_instance_id"] if "aws_instance_id" in issue else None, - ) - @staticmethod - def _handle_smb_password_issue(issue, instance_arn): +def _handle_ssh_issue(issue, instance_arn): + return _build_generic_finding( + severity=1, + title="Machines are accessible using SSH passwords supplied by the user during " + "the Monkey's configuration.", + description="Change {0}'s password to a complex one-use password that is not " + "shared with other computers on the " + "network.".format(issue["username"]), + recommendation="The machine {0} ({1}) is vulnerable to a SSH attack. The Monkey " + "authenticated over the SSH" + " protocol with user {2} and its " + "password.".format(issue["machine"], issue["ip_address"], issue["username"]), + instance_arn=instance_arn, + instance_id=issue["aws_instance_id"] if "aws_instance_id" in issue else None, + ) - return AWSExporter._build_generic_finding( - severity=1, - title="Machines are accessible using passwords supplied by the user during the " - "Monkey's configuration.", - description="Change {0}'s password to a complex one-use password that is not " - "shared with other computers on the " - "network.".format(issue["username"]), - recommendation="The machine {0} ({1}) is vulnerable to a SMB attack. The Monkey " - "authenticated over the SMB " - "protocol with user {2} and its password.".format( - issue["machine"], issue["ip_address"], issue["username"] - ), - instance_arn=instance_arn, - instance_id=issue["aws_instance_id"] if "aws_instance_id" in issue else None, - ) - @staticmethod - def _handle_wmi_password_issue(issue, instance_arn): +def _handle_ssh_key_issue(issue, instance_arn): + return _build_generic_finding( + severity=1, + title="Machines are accessible using SSH passwords supplied by the user during " + "the Monkey's configuration.", + description="Protect {ssh_key} private key with a pass phrase.".format( + ssh_key=issue["ssh_key"] + ), + recommendation="The machine {machine} ({ip_address}) is vulnerable to a SSH " + "attack. The Monkey authenticated " + "over the SSH protocol with private key {ssh_key}.".format( + machine=issue["machine"], ip_address=issue["ip_address"], ssh_key=issue["ssh_key"] + ), + instance_arn=instance_arn, + instance_id=issue["aws_instance_id"] if "aws_instance_id" in issue else None, + ) - return AWSExporter._build_generic_finding( - severity=1, - title="Machines are accessible using passwords supplied by the user during the " - "Monkey's configuration.", - description="Change {0}'s password to a complex one-use password that is not " - "shared with other computers on the " - "network.", - recommendation="The machine {machine} ({ip_address}) is vulnerable to a WMI " - "attack. The Monkey authenticated over " - "the WMI protocol with user {username} and its password.".format( - machine=issue["machine"], ip_address=issue["ip_address"], username=issue["username"] - ), - instance_arn=instance_arn, - instance_id=issue["aws_instance_id"] if "aws_instance_id" in issue else None, - ) - @staticmethod - def _handle_wmi_pth_issue(issue, instance_arn): +def _handle_island_cross_segment_issue(issue, instance_arn): + return _build_generic_finding( + severity=1, + title="Weak segmentation - Machines from different segments are able to " "communicate.", + description="Segment your network and make sure there is no communication between " + "machines from different " + "segments.", + recommendation="The network can probably be segmented. A monkey instance on \ + {0} in the networks {1} \ + could directly access the Monkey Island server in the networks {2}.".format( + issue["machine"], issue["networks"], issue["server_networks"] + ), + instance_arn=instance_arn, + instance_id=issue["aws_instance_id"] if "aws_instance_id" in issue else None, + ) - return AWSExporter._build_generic_finding( - severity=1, - title="Machines are accessible using passwords supplied by the user during the " - "Monkey's configuration.", - description="Change {0}'s password to a complex one-use password that is not " - "shared with other computers on the " - "network.".format(issue["username"]), - recommendation="The machine {machine} ({ip_address}) is vulnerable to a WMI " - "attack. The Monkey used a " - "pass-the-hash attack over WMI protocol with user {username}".format( - machine=issue["machine"], ip_address=issue["ip_address"], username=issue["username"] - ), - instance_arn=instance_arn, - instance_id=issue["aws_instance_id"] if "aws_instance_id" in issue else None, - ) - @staticmethod - def _handle_shared_passwords_domain_issue(issue, instance_arn): +def _handle_shared_passwords_issue(issue, instance_arn): + return _build_generic_finding( + severity=1, + title="Multiple users have the same password", + description="Some users are sharing passwords, this should be fixed by changing " + "passwords.", + recommendation="These users are sharing access password: {0}.".format(issue["shared_with"]), + instance_arn=instance_arn, + instance_id=issue["aws_instance_id"] if "aws_instance_id" in issue else None, + ) - return AWSExporter._build_generic_finding( - severity=1, - title="Multiple users have the same password.", - description="Some domain users are sharing passwords, this should be fixed by " - "changing passwords.", - recommendation="These users are sharing access password: {shared_with}.".format( - shared_with=issue["shared_with"] - ), - instance_arn=instance_arn, - instance_id=issue["aws_instance_id"] if "aws_instance_id" in issue else None, - ) - @staticmethod - def _handle_shared_admins_domain_issue(issue, instance_arn): +def _handle_smb_password_issue(issue, instance_arn): + return _build_generic_finding( + severity=1, + title="Machines are accessible using passwords supplied by the user during the " + "Monkey's configuration.", + description="Change {0}'s password to a complex one-use password that is not " + "shared with other computers on the " + "network.".format(issue["username"]), + recommendation="The machine {0} ({1}) is vulnerable to a SMB attack. The Monkey " + "authenticated over the SMB " + "protocol with user {2} and its password.".format( + issue["machine"], issue["ip_address"], issue["username"] + ), + instance_arn=instance_arn, + instance_id=issue["aws_instance_id"] if "aws_instance_id" in issue else None, + ) - return AWSExporter._build_generic_finding( - severity=1, - title="Shared local administrator account - Different machines have the same " - "account as a local administrator.", - description="Make sure the right administrator accounts are managing the right " - "machines, and that there isn't " - "an unintentional local admin sharing.", - recommendation="Here is a list of machines which the account {username} is " - "defined as an administrator: " - "{shared_machines}".format( - username=issue["username"], shared_machines=issue["shared_machines"] - ), - instance_arn=instance_arn, - instance_id=issue["aws_instance_id"] if "aws_instance_id" in issue else None, - ) - @staticmethod - def _handle_strong_users_on_crit_issue(issue, instance_arn): +def _handle_wmi_password_issue(issue, instance_arn): + return _build_generic_finding( + severity=1, + title="Machines are accessible using passwords supplied by the user during the " + "Monkey's configuration.", + description="Change {0}'s password to a complex one-use password that is not " + "shared with other computers on the " + "network.", + recommendation="The machine {machine} ({ip_address}) is vulnerable to a WMI " + "attack. The Monkey authenticated over " + "the WMI protocol with user {username} and its password.".format( + machine=issue["machine"], ip_address=issue["ip_address"], username=issue["username"] + ), + instance_arn=instance_arn, + instance_id=issue["aws_instance_id"] if "aws_instance_id" in issue else None, + ) - return AWSExporter._build_generic_finding( - severity=1, - title="Mimikatz found login credentials of a user who has admin access to a " - "server defined as critical.", - description="This critical machine is open to attacks via strong users with " - "access to it.", - recommendation="The services: {services} have been found on the machine thus " - "classifying it as a critical " - "machine. These users has access to it:{threatening_users}.".format( - services=issue["services"], threatening_users=issue["threatening_users"] - ), - instance_arn=instance_arn, - instance_id=issue["aws_instance_id"] if "aws_instance_id" in issue else None, - ) - @staticmethod - def _handle_hadoop_issue(issue, instance_arn): +def _handle_wmi_pth_issue(issue, instance_arn): + return _build_generic_finding( + severity=1, + title="Machines are accessible using passwords supplied by the user during the " + "Monkey's configuration.", + description="Change {0}'s password to a complex one-use password that is not " + "shared with other computers on the " + "network.".format(issue["username"]), + recommendation="The machine {machine} ({ip_address}) is vulnerable to a WMI " + "attack. The Monkey used a " + "pass-the-hash attack over WMI protocol with user {username}".format( + machine=issue["machine"], ip_address=issue["ip_address"], username=issue["username"] + ), + instance_arn=instance_arn, + instance_id=issue["aws_instance_id"] if "aws_instance_id" in issue else None, + ) - return AWSExporter._build_generic_finding( - severity=10, - title="Hadoop/Yarn servers are vulnerable to remote code execution.", - description="Run Hadoop in secure mode, add Kerberos authentication.", - recommendation="The Hadoop server at {machine} ({ip_address}) is vulnerable to " - "remote code execution attack." - "The attack was made possible due to default Hadoop/Yarn " - "configuration being insecure.", - instance_arn=instance_arn, - instance_id=issue["aws_instance_id"] if "aws_instance_id" in issue else None, - ) + +def _handle_shared_passwords_domain_issue(issue, instance_arn): + return _build_generic_finding( + severity=1, + title="Multiple users have the same password.", + description="Some domain users are sharing passwords, this should be fixed by " + "changing passwords.", + recommendation="These users are sharing access password: {shared_with}.".format( + shared_with=issue["shared_with"] + ), + instance_arn=instance_arn, + instance_id=issue["aws_instance_id"] if "aws_instance_id" in issue else None, + ) + + +def _handle_shared_admins_domain_issue(issue, instance_arn): + return _build_generic_finding( + severity=1, + title="Shared local administrator account - Different machines have the same " + "account as a local administrator.", + description="Make sure the right administrator accounts are managing the right " + "machines, and that there isn't " + "an unintentional local admin sharing.", + recommendation="Here is a list of machines which the account {username} is " + "defined as an administrator: " + "{shared_machines}".format( + username=issue["username"], shared_machines=issue["shared_machines"] + ), + instance_arn=instance_arn, + instance_id=issue["aws_instance_id"] if "aws_instance_id" in issue else None, + ) + + +def _handle_strong_users_on_crit_issue(issue, instance_arn): + return _build_generic_finding( + severity=1, + title="Mimikatz found login credentials of a user who has admin access to a " + "server defined as critical.", + description="This critical machine is open to attacks via strong users with " + "access to it.", + recommendation="The services: {services} have been found on the machine thus " + "classifying it as a critical " + "machine. These users has access to it:{threatening_users}.".format( + services=issue["services"], threatening_users=issue["threatening_users"] + ), + instance_arn=instance_arn, + instance_id=issue["aws_instance_id"] if "aws_instance_id" in issue else None, + ) + + +def _handle_hadoop_issue(issue, instance_arn): + return _build_generic_finding( + severity=10, + title="Hadoop/Yarn servers are vulnerable to remote code execution.", + description="Run Hadoop in secure mode, add Kerberos authentication.", + recommendation="The Hadoop server at {machine} ({ip_address}) is vulnerable to " + "remote code execution attack." + "The attack was made possible due to default Hadoop/Yarn " + "configuration being insecure.", + instance_arn=instance_arn, + instance_id=issue["aws_instance_id"] if "aws_instance_id" in issue else None, + ) diff --git a/monkey/monkey_island/cc/services/reporting/exporter.py b/monkey/monkey_island/cc/services/reporting/exporter.py deleted file mode 100644 index e79fabc07..000000000 --- a/monkey/monkey_island/cc/services/reporting/exporter.py +++ /dev/null @@ -1,7 +0,0 @@ -class Exporter(object): - def __init__(self): - pass - - @staticmethod - def handle_report(report_json): - raise NotImplementedError diff --git a/monkey/monkey_island/cc/services/reporting/exporter_init.py b/monkey/monkey_island/cc/services/reporting/exporter_init.py deleted file mode 100644 index bb2d568b9..000000000 --- a/monkey/monkey_island/cc/services/reporting/exporter_init.py +++ /dev/null @@ -1,28 +0,0 @@ -import logging - -from monkey_island.cc.services import aws_service -from monkey_island.cc.services.reporting.aws_exporter import AWSExporter -from monkey_island.cc.services.reporting.report_exporter_manager import ReportExporterManager - -logger = logging.getLogger(__name__) - - -def populate_exporter_list(): - manager = ReportExporterManager() - try_add_aws_exporter_to_manager(manager) - - if len(manager.get_exporters_list()) != 0: - logger.debug( - "Populated exporters list with the following exporters: {0}".format( - str(manager.get_exporters_list()) - ) - ) - - -def try_add_aws_exporter_to_manager(manager): - # noinspection PyBroadException - try: - if aws_service.is_on_aws(): - manager.add_exporter_to_list(AWSExporter) - except Exception: - logger.error("Failed adding aws exporter to manager. Exception info:", exc_info=True) diff --git a/monkey/monkey_island/cc/services/reporting/report.py b/monkey/monkey_island/cc/services/reporting/report.py index 6b2edc13e..69a252580 100644 --- a/monkey/monkey_island/cc/services/reporting/report.py +++ b/monkey/monkey_island/cc/services/reporting/report.py @@ -26,7 +26,6 @@ from monkey_island.cc.services.reporting.exploitations.monkey_exploitation impor get_monkey_exploited, ) from monkey_island.cc.services.reporting.pth_report import PTHReportService -from monkey_island.cc.services.reporting.report_exporter_manager import ReportExporterManager from monkey_island.cc.services.reporting.report_generation_synchronisation import ( safe_generate_regular_report, ) @@ -36,6 +35,8 @@ from monkey_island.cc.services.reporting.stolen_credentials import ( ) from monkey_island.cc.services.utils.network_utils import get_subnets, local_ip_addresses +from .. import AWSService +from . import aws_exporter from .issue_processing.exploit_processing.exploiter_descriptor_enum import ExploiterDescriptorEnum from .issue_processing.exploit_processing.processors.cred_exploit import CredentialType from .issue_processing.exploit_processing.processors.exploit import ExploiterReportInfo @@ -44,11 +45,18 @@ logger = logging.getLogger(__name__) class ReportService: + + _aws_service = None + class DerivedIssueEnum: WEAK_PASSWORD = "weak_password" STOLEN_CREDS = "stolen_creds" ZEROLOGON_PASS_RESTORE_FAILED = "zerologon_pass_restore_failed" + @classmethod + def initialize(cls, aws_service: AWSService): + cls._aws_service = aws_service + @staticmethod def get_first_monkey_time(): return ( @@ -488,8 +496,8 @@ class ReportService: "recommendations": {"issues": issues, "domain_issues": domain_issues}, "meta_info": {"latest_monkey_modifytime": monkey_latest_modify_time}, } - ReportExporterManager().export(report) save_report(report) + aws_exporter.handle_report(report, ReportService._aws_service.island_aws_instance) return report @staticmethod diff --git a/monkey/monkey_island/cc/services/reporting/report_exporter_manager.py b/monkey/monkey_island/cc/services/reporting/report_exporter_manager.py deleted file mode 100644 index a71679685..000000000 --- a/monkey/monkey_island/cc/services/reporting/report_exporter_manager.py +++ /dev/null @@ -1,24 +0,0 @@ -import logging - -from common.utils.code_utils import Singleton - -logger = logging.getLogger(__name__) - - -class ReportExporterManager(object, metaclass=Singleton): - def __init__(self): - self._exporters_set = set() - - def get_exporters_list(self): - return self._exporters_set - - def add_exporter_to_list(self, exporter): - self._exporters_set.add(exporter) - - def export(self, report): - for exporter in self._exporters_set: - logger.debug("Trying to export using " + repr(exporter)) - try: - exporter().handle_report(report) - except Exception as e: - logger.exception("Failed to export report, error: " + e) diff --git a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunOnAWS/AWSInstanceTable.js b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunOnAWS/AWSInstanceTable.js index cf792f7b8..5492d8ae5 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunOnAWS/AWSInstanceTable.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunOnAWS/AWSInstanceTable.js @@ -70,10 +70,11 @@ function AWSInstanceTable(props) { let color = 'inherit'; if (r) { let instId = r.original.instance_id; + let runResult = getRunResults(instId); if (isSelected(instId)) { color = '#ffed9f'; - } else if (Object.prototype.hasOwnProperty.call(props.results, instId)) { - color = props.results[instId] ? '#00f01b' : '#f00000' + } else if (runResult) { + color = runResult.status === 'error' ? '#f00000' : '#00f01b' } } @@ -82,6 +83,15 @@ function AWSInstanceTable(props) { }; } + function getRunResults(instanceId) { + for(let result of props.results){ + if (result.instance_id === instanceId){ + return result + } + } + return false + } + return (