Merge pull request #1919 from guardicore/1636-long-aws-check

1636 long aws check
This commit is contained in:
Mike Salvatore 2022-05-02 09:53:46 -04:00 committed by GitHub
commit 8d65fa36f2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 235 additions and 295 deletions

View File

@ -85,6 +85,7 @@ Changelog](https://keepachangelog.com/en/1.0.0/).
- A bug where windows executable was not self deleting. #1763 - A bug where windows executable was not self deleting. #1763
- Incorrect line number in the telemetry overview window on the Map page. #1850 - Incorrect line number in the telemetry overview window on the Map page. #1850
- Automatic jumping to the bottom in the telemetry overview windows. #1850 - Automatic jumping to the bottom in the telemetry overview windows. #1850
- 2-second delay when the Island server starts, and it's not running on AWS. #1636
### Security ### Security
- Change SSH exploiter so that it does not set the permissions of the agent - Change SSH exploiter so that it does not set the permissions of the agent

View File

@ -50,6 +50,10 @@ If your EC2 instances don't have the _SSM agent_ installed, they will not be abl
See [Amazon's documentation about working with SSM agents](https://docs.aws.amazon.com/systems-manager/latest/userguide/ssm-agent.html) for more details on how to check if you have an SSM agent and how to manually install one if you don't yet have it. See [Amazon's documentation about working with SSM agents](https://docs.aws.amazon.com/systems-manager/latest/userguide/ssm-agent.html) for more details on how to check if you have an SSM agent and how to manually install one if you don't yet have it.
### Firewall rules
Make sure that all machines that will run the Monkey agent can access the Island(port 5000).
## Usage ## Usage
### Running the Infection Monkey ### Running the Infection Monkey

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 54 KiB

View File

@ -1,11 +1,11 @@
import json import json
import logging import logging
import re import re
from dataclasses import dataclass
from typing import Optional, Tuple
import requests import requests
from common.cloud.instance import CloudInstance
AWS_INSTANCE_METADATA_LOCAL_IP_ADDRESS = "169.254.169.254" 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) AWS_LATEST_METADATA_URI_PREFIX = "http://{0}/latest/".format(AWS_INSTANCE_METADATA_LOCAL_IP_ADDRESS)
ACCOUNT_ID_KEY = "accountId" ACCOUNT_ID_KEY = "accountId"
@ -15,26 +15,50 @@ logger = logging.getLogger(__name__)
AWS_TIMEOUT = 2 AWS_TIMEOUT = 2
class AwsInstance(CloudInstance): @dataclass
class AwsInstanceInfo:
instance_id: Optional[str] = None
region: Optional[str] = None
account_id: Optional[str] = None
class AwsInstance:
""" """
Class which gives useful information about the current instance you're on. Class which gives useful information about the current instance you're on.
""" """
def is_instance(self):
return self.instance_id is not None
def __init__(self): def __init__(self):
self.instance_id = None self._is_instance, self._instance_info = AwsInstance._fetch_instance_info()
self.region = None
self.account_id = None
@property
def is_instance(self) -> bool:
return self._is_instance
@property
def instance_id(self) -> str:
return self._instance_info.instance_id
@property
def region(self) -> str:
return self._instance_info.region
@property
def account_id(self) -> str:
return self._instance_info.account_id
@staticmethod
def _fetch_instance_info() -> Tuple[bool, AwsInstanceInfo]:
try: try:
response = requests.get( response = requests.get(
AWS_LATEST_METADATA_URI_PREFIX + "meta-data/instance-id", AWS_LATEST_METADATA_URI_PREFIX + "meta-data/instance-id",
timeout=AWS_TIMEOUT, timeout=AWS_TIMEOUT,
) )
self.instance_id = response.text if response else None if not response:
self.region = self._parse_region( return False, AwsInstanceInfo()
info = AwsInstanceInfo()
info.instance_id = response.text if response else False
info.region = AwsInstance._parse_region(
requests.get( requests.get(
AWS_LATEST_METADATA_URI_PREFIX + "meta-data/placement/availability-zone", AWS_LATEST_METADATA_URI_PREFIX + "meta-data/placement/availability-zone",
timeout=AWS_TIMEOUT, timeout=AWS_TIMEOUT,
@ -42,9 +66,10 @@ class AwsInstance(CloudInstance):
) )
except (requests.RequestException, IOError) as e: except (requests.RequestException, IOError) as e:
logger.debug("Failed init of AwsInstance while getting metadata: {}".format(e)) logger.debug("Failed init of AwsInstance while getting metadata: {}".format(e))
return False, AwsInstanceInfo()
try: try:
self.account_id = self._extract_account_id( info.account_id = AwsInstance._extract_account_id(
requests.get( requests.get(
AWS_LATEST_METADATA_URI_PREFIX + "dynamic/instance-identity/document", AWS_LATEST_METADATA_URI_PREFIX + "dynamic/instance-identity/document",
timeout=AWS_TIMEOUT, timeout=AWS_TIMEOUT,
@ -54,6 +79,9 @@ class AwsInstance(CloudInstance):
logger.debug( logger.debug(
"Failed init of AwsInstance while getting dynamic instance data: {}".format(e) "Failed init of AwsInstance while getting dynamic instance data: {}".format(e)
) )
return False, AwsInstanceInfo()
return True, info
@staticmethod @staticmethod
def _parse_region(region_url_response): def _parse_region(region_url_response):
@ -68,12 +96,6 @@ class AwsInstance(CloudInstance):
else: else:
return None return None
def get_instance_id(self):
return self.instance_id
def get_region(self):
return self.region
@staticmethod @staticmethod
def _extract_account_id(instance_identity_document_response): def _extract_account_id(instance_identity_document_response):
""" """

View File

@ -1,74 +0,0 @@
import logging
import boto3
import botocore
from common.cloud.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]
]
class AwsService(object):
"""
A wrapper class around the boto3 client and session modules, which supplies various AWS
services.
This class will assume:
1. That it's running on an EC2 instance
2. That the instance is associated with the correct IAM role. See
https://boto3.amazonaws.com/v1/documentation/api/latest/guide/configuration.html#iam-role
for details.
"""
region = None
@staticmethod
def set_region(region):
AwsService.region = region
@staticmethod
def get_client(client_type, region=None):
return boto3.client(
client_type, region_name=region if region is not None else AwsService.region
)
@staticmethod
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
"""
current_instance = AwsInstance()
local_ssm_client = boto3.client("ssm", current_instance.get_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

View File

@ -1,9 +0,0 @@
class CloudInstance(object):
"""
This is an abstract class which represents a cloud instance.
The current machine can be a cloud instance (for example EC2 instance or Azure VM).
"""
def is_instance(self) -> bool:
raise NotImplementedError()

View File

@ -1,7 +1,3 @@
# abstract, static method decorator
# noinspection PyPep8Naming
class abstractstatic(staticmethod): class abstractstatic(staticmethod):
__slots__ = () __slots__ = ()
@ -10,3 +6,12 @@ class abstractstatic(staticmethod):
function.__isabstractmethod__ = True function.__isabstractmethod__ = True
__isabstractmethod__ = True __isabstractmethod__ = True
class Singleton(type):
_instances = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
return cls._instances[cls]

View File

@ -1,6 +1,6 @@
import logging import logging
from common.cloud.aws.aws_instance import AwsInstance from common.aws.aws_instance import AwsInstance
from infection_monkey.telemetry.aws_instance_telem import AWSInstanceTelemetry from infection_monkey.telemetry.aws_instance_telem import AWSInstanceTelemetry
from infection_monkey.telemetry.messengers.legacy_telemetry_messenger_adapter import ( from infection_monkey.telemetry.messengers.legacy_telemetry_messenger_adapter import (
LegacyTelemetryMessengerAdapter, LegacyTelemetryMessengerAdapter,
@ -10,18 +10,14 @@ from infection_monkey.utils.threading import create_daemon_thread
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def _running_on_aws(aws_instance: AwsInstance) -> bool:
return aws_instance.is_instance()
def _report_aws_environment(telemetry_messenger: LegacyTelemetryMessengerAdapter): def _report_aws_environment(telemetry_messenger: LegacyTelemetryMessengerAdapter):
logger.info("Collecting AWS info") logger.info("Collecting AWS info")
aws_instance = AwsInstance() aws_instance = AwsInstance()
if _running_on_aws(aws_instance): if aws_instance.is_instance:
logger.info("Machine is an AWS instance") logger.info("Machine is an AWS instance")
telemetry_messenger.send_telemetry(AWSInstanceTelemetry(aws_instance.get_instance_id())) telemetry_messenger.send_telemetry(AWSInstanceTelemetry(aws_instance.instance_id))
else: else:
logger.info("Machine is NOT an AWS instance") logger.info("Machine is NOT an AWS instance")

View File

@ -48,7 +48,6 @@ from monkey_island.cc.resources.zero_trust.finding_event import ZeroTrustFinding
from monkey_island.cc.resources.zero_trust.zero_trust_report import ZeroTrustReport from monkey_island.cc.resources.zero_trust.zero_trust_report import ZeroTrustReport
from monkey_island.cc.server_utils.consts import MONKEY_ISLAND_ABS_PATH from monkey_island.cc.server_utils.consts import MONKEY_ISLAND_ABS_PATH
from monkey_island.cc.server_utils.custom_json_encoder import CustomJSONEncoder from monkey_island.cc.server_utils.custom_json_encoder import CustomJSONEncoder
from monkey_island.cc.services.remote_run_aws import RemoteRunAwsService
from monkey_island.cc.services.representations import output_json from monkey_island.cc.services.representations import output_json
HOME_FILE = "index.html" HOME_FILE = "index.html"
@ -103,10 +102,6 @@ def init_app_services(app):
with app.app_context(): with app.app_context():
database.init() database.init()
# If running on AWS, this will initialize the instance data, which is used "later" in the
# execution of the island.
RemoteRunAwsService.init()
def init_app_url_rules(app): def init_app_url_rules(app):
app.add_url_rule("/", "serve_home", serve_home) app.add_url_rule("/", "serve_home", serve_home)

View File

@ -4,8 +4,8 @@ import flask_restful
from botocore.exceptions import ClientError, NoCredentialsError from botocore.exceptions import ClientError, NoCredentialsError
from flask import jsonify, make_response, request from flask import jsonify, make_response, request
from common.cloud.aws.aws_service import AwsService
from monkey_island.cc.resources.auth.auth import jwt_required 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.remote_run_aws import RemoteRunAwsService
CLIENT_ERROR_FORMAT = ( CLIENT_ERROR_FORMAT = (
@ -19,10 +19,6 @@ NO_CREDS_ERROR_FORMAT = (
class RemoteRun(flask_restful.Resource): class RemoteRun(flask_restful.Resource):
def __init__(self):
super(RemoteRun, self).__init__()
RemoteRunAwsService.init()
def run_aws_monkeys(self, request_body): def run_aws_monkeys(self, request_body):
instances = request_body.get("instances") instances = request_body.get("instances")
island_ip = request_body.get("island_ip") island_ip = request_body.get("island_ip")
@ -32,11 +28,11 @@ class RemoteRun(flask_restful.Resource):
def get(self): def get(self):
action = request.args.get("action") action = request.args.get("action")
if action == "list_aws": if action == "list_aws":
is_aws = RemoteRunAwsService.is_running_on_aws() is_aws = aws_service.is_on_aws()
resp = {"is_aws": is_aws} resp = {"is_aws": is_aws}
if is_aws: if is_aws:
try: try:
resp["instances"] = AwsService.get_instances() resp["instances"] = aws_service.get_instances()
except NoCredentialsError as e: except NoCredentialsError as e:
resp["error"] = NO_CREDS_ERROR_FORMAT.format(e) resp["error"] = NO_CREDS_ERROR_FORMAT.format(e)
return jsonify(resp) return jsonify(resp)
@ -52,7 +48,6 @@ class RemoteRun(flask_restful.Resource):
body = json.loads(request.data) body = json.loads(request.data)
resp = {} resp = {}
if body.get("type") == "aws": if body.get("type") == "aws":
RemoteRunAwsService.update_aws_region_authless()
result = self.run_aws_monkeys(body) result = self.run_aws_monkeys(body)
resp["result"] = result resp["result"] = result
return jsonify(resp) return jsonify(resp)

View File

@ -3,6 +3,7 @@ import json
import logging import logging
import sys import sys
from pathlib import Path from pathlib import Path
from threading import Thread
import gevent.hub import gevent.hub
from gevent.pywsgi import WSGIServer from gevent.pywsgi import WSGIServer
@ -131,7 +132,8 @@ def _configure_gevent_exception_handling(data_dir):
def _start_island_server( def _start_island_server(
should_setup_only: bool, config_options: IslandConfigOptions, container: DIContainer should_setup_only: bool, config_options: IslandConfigOptions, container: DIContainer
): ):
populate_exporter_list() # 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) app = init_app(mongo_setup.MONGO_URL, container)
if should_setup_only: if should_setup_only:

View File

@ -1,10 +1,10 @@
import logging import logging
import time import time
from common.cloud.aws.aws_service import AwsService
from common.cmd.aws.aws_cmd_result import AwsCmdResult
from common.cmd.cmd_runner import CmdRunner from common.cmd.cmd_runner import CmdRunner
from common.cmd.cmd_status import CmdStatus 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__) logger = logging.getLogger(__name__)
@ -18,7 +18,7 @@ class AwsCmdRunner(CmdRunner):
super(AwsCmdRunner, self).__init__(is_linux) super(AwsCmdRunner, self).__init__(is_linux)
self.instance_id = instance_id self.instance_id = instance_id
self.region = region self.region = region
self.ssm = AwsService.get_client("ssm", region) self.ssm = aws_service.get_client("ssm", region)
def query_command(self, command_id): def query_command(self, command_id):
time.sleep(2) time.sleep(2)

View File

@ -0,0 +1,99 @@
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

View File

@ -1,7 +1,8 @@
from pathlib import Path from pathlib import Path
from threading import Thread
from common import DIContainer from common import DIContainer
from monkey_island.cc.services import DirectoryFileStorageService, IFileStorageService from monkey_island.cc.services import DirectoryFileStorageService, IFileStorageService, aws_service
from monkey_island.cc.services.post_breach_files import PostBreachFilesService from monkey_island.cc.services.post_breach_files import PostBreachFilesService
from monkey_island.cc.services.run_local_monkey import LocalMonkeyRunService from monkey_island.cc.services.run_local_monkey import LocalMonkeyRunService
@ -14,6 +15,9 @@ def initialize_services(data_dir: Path) -> DIContainer:
IFileStorageService, DirectoryFileStorageService(data_dir / "custom_pbas") 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()
# This is temporary until we get DI all worked out. # This is temporary until we get DI all worked out.
PostBreachFilesService.initialize(container.resolve(IFileStorageService)) PostBreachFilesService.initialize(container.resolve(IFileStorageService))
LocalMonkeyRunService.initialize(data_dir) LocalMonkeyRunService.initialize(data_dir)

View File

@ -1,38 +1,13 @@
import logging import logging
from common.cloud.aws.aws_instance import AwsInstance
from common.cloud.aws.aws_service import AwsService
from common.cmd.aws.aws_cmd_runner import AwsCmdRunner
from common.cmd.cmd import Cmd from common.cmd.cmd import Cmd
from common.cmd.cmd_runner import CmdRunner from common.cmd.cmd_runner import CmdRunner
from monkey_island.cc.server_utils.aws_cmd_runner import AwsCmdRunner
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class RemoteRunAwsService: class RemoteRunAwsService:
aws_instance = None
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.try_init_aws_instance()
@staticmethod
def try_init_aws_instance():
# noinspection PyBroadException
try:
RemoteRunAwsService.aws_instance = AwsInstance()
except Exception:
logger.error("Failed init aws instance. Exception info: ", exc_info=True)
@staticmethod @staticmethod
def run_aws_monkeys(instances, island_ip): def run_aws_monkeys(instances, island_ip):
""" """
@ -51,17 +26,6 @@ class RemoteRunAwsService:
lambda _, result: result.is_success, lambda _, result: result.is_success,
) )
@staticmethod
def is_running_on_aws():
return RemoteRunAwsService.aws_instance.is_instance()
@staticmethod
def update_aws_region_authless():
"""
Updates the AWS region without auth params (via IAM role)
"""
AwsService.set_region(RemoteRunAwsService.aws_instance.region)
@staticmethod @staticmethod
def _run_aws_monkey_cmd_async(instance_id, is_linux, island_ip): def _run_aws_monkey_cmd_async(instance_id, is_linux, island_ip):
""" """

View File

@ -5,7 +5,7 @@ from datetime import datetime
import boto3 import boto3
from botocore.exceptions import UnknownServiceError from botocore.exceptions import UnknownServiceError
from common.cloud.aws.aws_instance import AwsInstance from monkey_island.cc.services import aws_service
from monkey_island.cc.services.reporting.exporter import Exporter from monkey_island.cc.services.reporting.exporter import Exporter
__authors__ = ["maor.rayzin", "shay.nehmad"] __authors__ = ["maor.rayzin", "shay.nehmad"]
@ -35,8 +35,7 @@ class AWSExporter(Exporter):
logger.info("No issues were found by the monkey, no need to send anything") logger.info("No issues were found by the monkey, no need to send anything")
return True return True
# Not suppressing error here on purpose. current_aws_region = aws_service.get_region()
current_aws_region = AwsInstance().get_region()
for machine in issues_list: for machine in issues_list:
for issue in issues_list[machine]: for issue in issues_list[machine]:
@ -90,7 +89,7 @@ class AWSExporter(Exporter):
) )
instance_arn = "arn:aws:ec2:" + str(region) + ":instance:{instance_id}" instance_arn = "arn:aws:ec2:" + str(region) + ":instance:{instance_id}"
# Not suppressing error here on purpose. # Not suppressing error here on purpose.
account_id = AwsInstance().get_account_id() account_id = aws_service.get_account_id()
logger.debug("aws account id acquired: {}".format(account_id)) logger.debug("aws account id acquired: {}".format(account_id))
aws_finding = { aws_finding = {

View File

@ -1,6 +1,6 @@
import logging import logging
from monkey_island.cc.services.remote_run_aws import RemoteRunAwsService from monkey_island.cc.services import aws_service
from monkey_island.cc.services.reporting.aws_exporter import AWSExporter from monkey_island.cc.services.reporting.aws_exporter import AWSExporter
from monkey_island.cc.services.reporting.report_exporter_manager import ReportExporterManager from monkey_island.cc.services.reporting.report_exporter_manager import ReportExporterManager
@ -22,8 +22,7 @@ def populate_exporter_list():
def try_add_aws_exporter_to_manager(manager): def try_add_aws_exporter_to_manager(manager):
# noinspection PyBroadException # noinspection PyBroadException
try: try:
RemoteRunAwsService.init() if aws_service.is_on_aws():
if RemoteRunAwsService.is_running_on_aws():
manager.add_exporter_to_list(AWSExporter) manager.add_exporter_to_list(AWSExporter)
except Exception: except Exception:
logger.error("Failed adding aws exporter to manager. Exception info:", exc_info=True) logger.error("Failed adding aws exporter to manager. Exception info:", exc_info=True)

View File

@ -1,17 +1,10 @@
import logging import logging
from common.utils.code_utils import Singleton
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class Singleton(type):
_instances = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
return cls._instances[cls]
class ReportExporterManager(object, metaclass=Singleton): class ReportExporterManager(object, metaclass=Singleton):
def __init__(self): def __init__(self):
self._exporters_set = set() self._exporters_set = set()

View File

@ -4,7 +4,6 @@ import platform
import sys import sys
block_cipher = None block_cipher = None
@ -13,7 +12,8 @@ def main():
# The format of the tuples is (src, dest_dir). See https://pythonhosted.org/PyInstaller/spec-files.html#adding-data-files # The format of the tuples is (src, dest_dir). See https://pythonhosted.org/PyInstaller/spec-files.html#adding-data-files
added_datas = [ added_datas = [
("../common/BUILD", "/common"), ("../common/BUILD", "/common"),
("../monkey_island/cc/setup/mongo/attack_mitigations.json", "/monkey_island/cc/setup/mongo/attack_mitigations.json") ("../monkey_island/cc/setup/mongo/attack_mitigations.json",
"/monkey_island/cc/setup/mongo/attack_mitigations.json")
] ]
a = Analysis(['main.py'], a = Analysis(['main.py'],

View File

@ -1,8 +1,10 @@
from types import MappingProxyType
import pytest import pytest
import requests import requests
import requests_mock import requests_mock
from common.cloud.aws.aws_instance import AWS_LATEST_METADATA_URI_PREFIX, AwsInstance from common.aws.aws_instance import AWS_LATEST_METADATA_URI_PREFIX, AwsInstance
INSTANCE_ID_RESPONSE = "i-1234567890abcdef0" INSTANCE_ID_RESPONSE = "i-1234567890abcdef0"
@ -37,8 +39,8 @@ EXPECTED_ACCOUNT_ID = "123456789012"
def get_test_aws_instance( def get_test_aws_instance(
text={"instance_id": None, "region": None, "account_id": None}, text=MappingProxyType({"instance_id": None, "region": None, "account_id": None}),
exception={"instance_id": None, "region": None, "account_id": None}, exception=MappingProxyType({"instance_id": None, "region": None, "account_id": None}),
): ):
with requests_mock.Mocker() as m: with requests_mock.Mocker() as m:
# request made to get instance_id # request made to get instance_id
@ -64,124 +66,97 @@ def get_test_aws_instance(
# all good data # all good data
@pytest.fixture @pytest.fixture
def good_data_mock_instance(): def good_data_mock_instance():
return get_test_aws_instance( instance = get_test_aws_instance(
text={ text={
"instance_id": INSTANCE_ID_RESPONSE, "instance_id": INSTANCE_ID_RESPONSE,
"region": AVAILABILITY_ZONE_RESPONSE, "region": AVAILABILITY_ZONE_RESPONSE,
"account_id": INSTANCE_IDENTITY_DOCUMENT_RESPONSE, "account_id": INSTANCE_IDENTITY_DOCUMENT_RESPONSE,
} }
) )
yield instance
del instance
def test_is_instance_good_data(good_data_mock_instance): def test_is_instance_good_data(good_data_mock_instance):
assert good_data_mock_instance.is_instance() assert good_data_mock_instance.is_instance
def test_get_instance_id_good_data(good_data_mock_instance): def test_instance_id_good_data(good_data_mock_instance):
assert good_data_mock_instance.get_instance_id() == EXPECTED_INSTANCE_ID assert good_data_mock_instance.instance_id == EXPECTED_INSTANCE_ID
def test_get_region_good_data(good_data_mock_instance): def test_region_good_data(good_data_mock_instance):
assert good_data_mock_instance.get_region() == EXPECTED_REGION assert good_data_mock_instance.region == EXPECTED_REGION
def test_get_account_id_good_data(good_data_mock_instance): def test_account_id_good_data(good_data_mock_instance):
assert good_data_mock_instance.get_account_id() == EXPECTED_ACCOUNT_ID assert good_data_mock_instance.account_id == EXPECTED_ACCOUNT_ID
# 'region' bad data # 'region' bad data
@pytest.fixture @pytest.fixture
def bad_region_data_mock_instance(): def bad_region_data_mock_instance():
return get_test_aws_instance( instance = get_test_aws_instance(
text={ text={
"instance_id": INSTANCE_ID_RESPONSE, "instance_id": INSTANCE_ID_RESPONSE,
"region": "in-a-different-world", "region": "in-a-different-world",
"account_id": INSTANCE_IDENTITY_DOCUMENT_RESPONSE, "account_id": INSTANCE_IDENTITY_DOCUMENT_RESPONSE,
} }
) )
yield instance
del instance
def test_is_instance_bad_region_data(bad_region_data_mock_instance): def test_is_instance_bad_region_data(bad_region_data_mock_instance):
assert bad_region_data_mock_instance.is_instance() assert bad_region_data_mock_instance.is_instance
def test_get_instance_id_bad_region_data(bad_region_data_mock_instance): def test_instance_id_bad_region_data(bad_region_data_mock_instance):
assert bad_region_data_mock_instance.get_instance_id() == EXPECTED_INSTANCE_ID assert bad_region_data_mock_instance.instance_id == EXPECTED_INSTANCE_ID
def test_get_region_bad_region_data(bad_region_data_mock_instance): def test_region_bad_region_data(bad_region_data_mock_instance):
assert bad_region_data_mock_instance.get_region() is None assert bad_region_data_mock_instance.region is None
def test_get_account_id_bad_region_data(bad_region_data_mock_instance): def test_account_id_bad_region_data(bad_region_data_mock_instance):
assert bad_region_data_mock_instance.get_account_id() == EXPECTED_ACCOUNT_ID assert bad_region_data_mock_instance.account_id == EXPECTED_ACCOUNT_ID
# 'account_id' bad data # 'account_id' bad data
@pytest.fixture @pytest.fixture
def bad_account_id_data_mock_instance(): def bad_account_id_data_mock_instance():
return get_test_aws_instance( instance = get_test_aws_instance(
text={ text={
"instance_id": INSTANCE_ID_RESPONSE, "instance_id": INSTANCE_ID_RESPONSE,
"region": AVAILABILITY_ZONE_RESPONSE, "region": AVAILABILITY_ZONE_RESPONSE,
"account_id": "who-am-i", "account_id": "who-am-i",
} }
) )
yield instance
del instance
def test_is_instance_bad_account_id_data(bad_account_id_data_mock_instance): def test_is_instance_bad_account_id_data(bad_account_id_data_mock_instance):
assert bad_account_id_data_mock_instance.is_instance() assert not bad_account_id_data_mock_instance.is_instance
def test_get_instance_id_bad_account_id_data(bad_account_id_data_mock_instance): def test_instance_id_bad_account_id_data(bad_account_id_data_mock_instance):
assert bad_account_id_data_mock_instance.get_instance_id() == EXPECTED_INSTANCE_ID assert bad_account_id_data_mock_instance.instance_id is None
def test_get_region_bad_account_id_data(bad_account_id_data_mock_instance): def test_region_bad_account_id_data(bad_account_id_data_mock_instance):
assert bad_account_id_data_mock_instance.get_region() == EXPECTED_REGION assert bad_account_id_data_mock_instance.region is None
def test_get_account_id_data_bad_account_id_data(bad_account_id_data_mock_instance): def test_account_id_data_bad_account_id_data(bad_account_id_data_mock_instance):
assert bad_account_id_data_mock_instance.get_account_id() is None assert bad_account_id_data_mock_instance.account_id is None
# 'instance_id' bad requests
@pytest.fixture
def bad_instance_id_request_mock_instance(instance_id_exception):
return get_test_aws_instance(
text={
"instance_id": None,
"region": AVAILABILITY_ZONE_RESPONSE,
"account_id": INSTANCE_IDENTITY_DOCUMENT_RESPONSE,
},
exception={"instance_id": instance_id_exception, "region": None, "account_id": None},
)
@pytest.mark.parametrize("instance_id_exception", [requests.RequestException, IOError])
def test_is_instance_bad_instance_id_request(bad_instance_id_request_mock_instance):
assert bad_instance_id_request_mock_instance.is_instance() is False
@pytest.mark.parametrize("instance_id_exception", [requests.RequestException, IOError])
def test_get_instance_id_bad_instance_id_request(bad_instance_id_request_mock_instance):
assert bad_instance_id_request_mock_instance.get_instance_id() is None
@pytest.mark.parametrize("instance_id_exception", [requests.RequestException, IOError])
def test_get_region_bad_instance_id_request(bad_instance_id_request_mock_instance):
assert bad_instance_id_request_mock_instance.get_region() is None
@pytest.mark.parametrize("instance_id_exception", [requests.RequestException, IOError])
def test_get_account_id_bad_instance_id_request(bad_instance_id_request_mock_instance):
assert bad_instance_id_request_mock_instance.get_account_id() == EXPECTED_ACCOUNT_ID
# 'region' bad requests # 'region' bad requests
@pytest.fixture @pytest.fixture
def bad_region_request_mock_instance(region_exception): def bad_region_request_mock_instance(region_exception):
return get_test_aws_instance( instance = get_test_aws_instance(
text={ text={
"instance_id": INSTANCE_ID_RESPONSE, "instance_id": INSTANCE_ID_RESPONSE,
"region": None, "region": None,
@ -189,59 +164,28 @@ def bad_region_request_mock_instance(region_exception):
}, },
exception={"instance_id": None, "region": region_exception, "account_id": None}, exception={"instance_id": None, "region": region_exception, "account_id": None},
) )
yield instance
del instance
@pytest.mark.parametrize("region_exception", [requests.RequestException, IOError]) @pytest.mark.parametrize("region_exception", [requests.RequestException, IOError])
def test_is_instance_bad_region_request(bad_region_request_mock_instance): def test_is_instance_bad_region_request(bad_region_request_mock_instance):
assert bad_region_request_mock_instance.is_instance() assert not bad_region_request_mock_instance.is_instance
@pytest.mark.parametrize("region_exception", [requests.RequestException, IOError]) @pytest.mark.parametrize("region_exception", [requests.RequestException, IOError])
def test_get_instance_id_bad_region_request(bad_region_request_mock_instance): def test_instance_id_bad_region_request(bad_region_request_mock_instance):
assert bad_region_request_mock_instance.get_instance_id() == EXPECTED_INSTANCE_ID assert bad_region_request_mock_instance.instance_id is None
@pytest.mark.parametrize("region_exception", [requests.RequestException, IOError]) @pytest.mark.parametrize("region_exception", [requests.RequestException, IOError])
def test_get_region_bad_region_request(bad_region_request_mock_instance): def test_region_bad_region_request(bad_region_request_mock_instance):
assert bad_region_request_mock_instance.get_region() is None assert bad_region_request_mock_instance.region is None
@pytest.mark.parametrize("region_exception", [requests.RequestException, IOError]) @pytest.mark.parametrize("region_exception", [requests.RequestException, IOError])
def test_get_account_id_bad_region_request(bad_region_request_mock_instance): def test_account_id_bad_region_request(bad_region_request_mock_instance):
assert bad_region_request_mock_instance.get_account_id() == EXPECTED_ACCOUNT_ID assert bad_region_request_mock_instance.account_id is None
# 'account_id' bad requests
@pytest.fixture
def bad_account_id_request_mock_instance(account_id_exception):
return get_test_aws_instance(
text={
"instance_id": INSTANCE_ID_RESPONSE,
"region": AVAILABILITY_ZONE_RESPONSE,
"account_id": None,
},
exception={"instance_id": None, "region": None, "account_id": account_id_exception},
)
@pytest.mark.parametrize("account_id_exception", [requests.RequestException, IOError])
def test_is_instance_bad_account_id_request(bad_account_id_request_mock_instance):
assert bad_account_id_request_mock_instance.is_instance()
@pytest.mark.parametrize("account_id_exception", [requests.RequestException, IOError])
def test_get_instance_id_bad_account_id_request(bad_account_id_request_mock_instance):
assert bad_account_id_request_mock_instance.get_instance_id() == EXPECTED_INSTANCE_ID
@pytest.mark.parametrize("account_id_exception", [requests.RequestException, IOError])
def test_get_region_bad_account_id_request(bad_account_id_request_mock_instance):
assert bad_account_id_request_mock_instance.get_region() == EXPECTED_REGION
@pytest.mark.parametrize("account_id_exception", [requests.RequestException, IOError])
def test_get_account_id_bad_account_id_request(bad_account_id_request_mock_instance):
assert bad_account_id_request_mock_instance.get_account_id() is None
# not found request # not found request
@ -261,20 +205,21 @@ def not_found_request_mock_instance():
m.get(url) m.get(url)
not_found_aws_instance_object = AwsInstance() not_found_aws_instance_object = AwsInstance()
return not_found_aws_instance_object yield not_found_aws_instance_object
del not_found_aws_instance_object
def test_is_instance_not_found_request(not_found_request_mock_instance): def test_is_instance_not_found_request(not_found_request_mock_instance):
assert not_found_request_mock_instance.is_instance() is False assert not_found_request_mock_instance.is_instance is False
def test_get_instance_id_not_found_request(not_found_request_mock_instance): def test_instance_id_not_found_request(not_found_request_mock_instance):
assert not_found_request_mock_instance.get_instance_id() is None assert not_found_request_mock_instance.instance_id is None
def test_get_region_not_found_request(not_found_request_mock_instance): def test_region_not_found_request(not_found_request_mock_instance):
assert not_found_request_mock_instance.get_region() is None assert not_found_request_mock_instance.region is None
def test_get_account_id_not_found_request(not_found_request_mock_instance): def test_account_id_not_found_request(not_found_request_mock_instance):
assert not_found_request_mock_instance.get_account_id() is None assert not_found_request_mock_instance.account_id is None

View File

@ -1,7 +1,7 @@
import json import json
from unittest import TestCase from unittest import TestCase
from common.cloud.aws.aws_service import filter_instance_data_from_aws_response from monkey_island.cc.services.aws_service import filter_instance_data_from_aws_response
class TestAwsService(TestCase): class TestAwsService(TestCase):