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/services/initialize.py b/monkey/monkey_island/cc/services/initialize.py index 8e0540329..78846cff5 100644 --- a/monkey/monkey_island/cc/services/initialize.py +++ b/monkey/monkey_island/cc/services/initialize.py @@ -7,6 +7,7 @@ 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: @@ -22,5 +23,6 @@ def initialize_services(data_dir: Path) -> DIContainer: 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/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 5eb640ee7..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 f9e90831e..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 @@ -74,7 +74,7 @@ function AWSInstanceTable(props) { if (isSelected(instId)) { color = '#ffed9f'; } else if (runResult) { - color = runResult.status === "error" ? '#f00000' : '#00f01b' + color = runResult.status === 'error' ? '#f00000' : '#00f01b' } }