From 7179d840a76e8042f51320b543528ad3e3d994c2 Mon Sep 17 00:00:00 2001 From: "maor.rayzin" Date: Mon, 19 Nov 2018 15:40:16 +0200 Subject: [PATCH 01/46] adding the exporter father class and aws implement --- monkey/monkey_island/cc/resources/aws_exporter.py | 9 +++++++++ monkey/monkey_island/cc/resources/exporter.py | 9 +++++++++ 2 files changed, 18 insertions(+) create mode 100644 monkey/monkey_island/cc/resources/aws_exporter.py create mode 100644 monkey/monkey_island/cc/resources/exporter.py diff --git a/monkey/monkey_island/cc/resources/aws_exporter.py b/monkey/monkey_island/cc/resources/aws_exporter.py new file mode 100644 index 000000000..cca47d968 --- /dev/null +++ b/monkey/monkey_island/cc/resources/aws_exporter.py @@ -0,0 +1,9 @@ +from exporter import Exporter + +class AWSExporter(Exporter): + + def __init__(self): + Exporter.__init__(self) + + def handle_report(self, report_json): + pass \ No newline at end of file diff --git a/monkey/monkey_island/cc/resources/exporter.py b/monkey/monkey_island/cc/resources/exporter.py new file mode 100644 index 000000000..98f3e7662 --- /dev/null +++ b/monkey/monkey_island/cc/resources/exporter.py @@ -0,0 +1,9 @@ + + +class Exporter: + + def __init__(self): + pass + + def handle_report(self, report_json): + raise NotImplementedError From 271c024574b83fc45418329b85ef03faef629b0c Mon Sep 17 00:00:00 2001 From: "maor.rayzin" Date: Sun, 25 Nov 2018 12:39:47 +0200 Subject: [PATCH 02/46] * Added env' config * Added exporters and aws exporter * changed report generation to be automatic on monkey death with support of on-demand report generation and mongo storage --- .../cc/environment/environment.py | 2 + .../cc/resources/aws_exporter.py | 294 +++++++++++++++++- monkey/monkey_island/cc/resources/exporter.py | 4 +- monkey/monkey_island/cc/resources/root.py | 2 + monkey/monkey_island/cc/services/node.py | 4 + monkey/monkey_island/cc/services/report.py | 62 +++- monkey/monkey_island/requirements.txt | 1 + 7 files changed, 345 insertions(+), 24 deletions(-) diff --git a/monkey/monkey_island/cc/environment/environment.py b/monkey/monkey_island/cc/environment/environment.py index 9e89208ef..70fc025c3 100644 --- a/monkey/monkey_island/cc/environment/environment.py +++ b/monkey/monkey_island/cc/environment/environment.py @@ -5,6 +5,8 @@ import aws logger = logging.getLogger(__name__) +AWS = 'aws' +STANDARD = 'standard' ENV_DICT = { 'standard': standard.StandardEnvironment, diff --git a/monkey/monkey_island/cc/resources/aws_exporter.py b/monkey/monkey_island/cc/resources/aws_exporter.py index cca47d968..363114948 100644 --- a/monkey/monkey_island/cc/resources/aws_exporter.py +++ b/monkey/monkey_island/cc/resources/aws_exporter.py @@ -1,9 +1,293 @@ -from exporter import Exporter +import logging +import uuid +from datetime import datetime +import boto3 + +from cc.resources.exporter import Exporter + +logger = logging.getLogger(__name__) + class AWSExporter(Exporter): - def __init__(self): - Exporter.__init__(self) + @staticmethod + def handle_report(report_json): - def handle_report(self, report_json): - pass \ No newline at end of file + findings_list = [] + issues_list = report_json['recommendations']['issues'] + for machine in issues_list: + for issue in issues_list[machine]: + findings_list.append(AWSExporter._prepare_finding(issue)) + + if not AWSExporter._send_findings(findings_list): + logger.error('Exporting findings to aws failed') + return False + + 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 + + @staticmethod + def _prepare_finding(issue): + findings_dict = { + 'island_cross_segment': AWSExporter._handle_island_cross_segment_issue, + 'ssh': AWSExporter._handle_ssh_issue, + 'shellshock': AWSExporter._handle_shellshock_issue, + 'tunnel': AWSExporter._handle_tunnel_issue, + 'elastic': AWSExporter._handle_elastic_issue, + 'smb_password': AWSExporter._handle_smb_password_issue, + 'smb_pth': AWSExporter._handle_smb_pth_issue, + 'sambacry': AWSExporter._handle_sambacry_issue, + 'shared_passwords': AWSExporter._handle_shared_passwords_issue, + } + + finding = { + "SchemaVersion": "2018-10-08", + "Id": uuid.uuid4().hex, + "ProductArn": "arn:aws:securityhub:us-west-2:324264561773:product/aws/guardduty", + "GeneratorId": issue['type'], + "AwsAccountId": "324264561773", + "Types": [ + "Software and Configuration Checks/Vulnerabilities/CVE" + ], + "CreatedAt": datetime.now().isoformat() + 'Z', + "UpdatedAt": datetime.now().isoformat() + 'Z', + } + return AWSExporter.merge_two_dicts(finding, findings_dict[issue['type']](issue)) + + @staticmethod + def _send_findings(findings_list): + + securityhub = boto3.client('securityhub') + import_response = securityhub.batch_import_findings(Findings=findings_list) + print import_response + if import_response['ResponseMetadata']['HTTPStatusCode'] == 200: + return True + else: + return False + + @staticmethod + def _handle_tunnel_issue(issue): + finding =\ + { + "Severity": { + "Product": 5, + "Normalized": 100 + }, + "Resources": [{ + "Type": "IpAddress", + "Id": issue['dest'] + }], + "RecordState": "ACTIVE", + } + + finding["Title"] = "Weak segmentation - Machines were able to communicate over unused ports." + finding["Description"] = "Use micro-segmentation policies to disable communication other than the required." + finding["Remediation"] = { + "Recommendation": { + "Text": "Machines are not locked down at port level. Network tunnel was set up from {0} to {1}" + .format(issue['machine'], issue['dest']) + } + } + return finding + + @staticmethod + def _handle_sambacry_issue(issue): + finding = \ + { + "Severity": { + "Product": 10, + "Normalized": 100 + }, + "Resources": [{ + "Type": "IpAddress", + "Id": str(issue['ip_address']) + }], + "RecordState": "ACTIVE", + } + + finding["Title"] = "Samba servers are vulnerable to 'SambaCry'" + finding["Description"] = "Change {0} password to a complex one-use password that is not shared with other computers on the network. Update your Samba server to 4.4.14 and up, 4.5.10 and up, or 4.6.4 and up."\ + .format(issue['username']) + finding["Remediation"] = { + "Recommendation": { + "Text": "The machine {0} ({1}) is vulnerable to a SambaCry attack. The Monkey authenticated over the SMB protocol with user {2} and its password, and used the SambaCry vulnerability.".format(issue['machine'], issue['ip_address'], issue['username']) + } + } + return finding + + @staticmethod + def _handle_smb_pth_issue(issue): + finding = \ + { + "Severity": { + "Product": 5, + "Normalized": 100 + }, + "Resources": [{ + "Type": "IpAddress", + "Id": issue['ip_address'] + }], + "RecordState": "ACTIVE", + } + + finding["Title"] = "Machines are accessible using passwords supplied by the user during the Monkey's configuration." + finding["Description"] = "Change {0}'s password to a complex one-use password that is not shared with other computers on the network.".format(issue['username']) + finding["Remediation"] = { + "Recommendation": { + "Text": "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']) + } + } + return finding + + @staticmethod + def _handle_ssh_issue(issue): + finding = \ + { + "Severity": { + "Product": 1, + "Normalized": 100 + }, + "Resources": [{ + "Type": "IpAddress", + "Id": issue['ip_address'] + }], + "RecordState": "ACTIVE", + } + + finding["Title"] = "Machines are accessible using SSH passwords supplied by the user during the Monkey's configuration." + finding["Description"] = "Change {0}'s password to a complex one-use password that is not shared with other computers on the network.".format(issue['username']) + finding["Remediation"] = { + "Recommendation": { + "Text": "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']) + } + } + return finding + + @staticmethod + def _handle_elastic_issue(issue): + finding = \ + { + "Severity": { + "Product": 10, + "Normalized": 100 + }, + "Resources": [{ + "Type": "IpAddress", + "Id": issue['ip_address'] + }], + "RecordState": "ACTIVE", + } + + finding["Title"] = "Elasticsearch servers are vulnerable to CVE-2015-1427" + finding["Description"] = "Update your Elastic Search server to version 1.4.3 and up." + finding["Remediation"] = { + "Recommendation": { + "Text": "The machine {0}({1}) is vulnerable to an Elastic Groovy attack. The attack was made possible because the Elastic Search server was not patched against CVE-2015-1427.".format(issue['machine'], issue['ip_address']) + } + } + return finding + + @staticmethod + def _handle_island_cross_segment_issue(issue): + finding = \ + { + "Severity": { + "Product": 1, + "Normalized": 100 + }, + "Resources": [{ + "Type": "IpAddress", + "Id": issue['networks'][0][:-2] + }], + "RecordState": "ACTIVE", + } + + finding["Title"] = "Weak segmentation - Machines from different segments are able to communicate." + finding["Description"] = "egment your network and make sure there is no communication between machines from different segments." + finding["Remediation"] = { + "Recommendation": { + "Text": "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']) + } + } + return finding + + @staticmethod + def _handle_shared_passwords_issue(issue): + finding = \ + { + "Severity": { + "Product": 1, + "Normalized": 100 + }, + "Resources": [{ + "Type": "IpAddress", + "Id": '10.0.0.1' + }], + "RecordState": "ACTIVE", + } + + finding["Title"] = "Multiple users have the same password" + finding["Description"] = "Some users are sharing passwords, this should be fixed by changing passwords." + finding["Remediation"] = { + "Recommendation": { + "Text": "These users are sharing access password: {0}.".format(issue['shared_with']) + } + } + return finding + + @staticmethod + def _handle_shellshock_issue(issue): + finding = \ + { + "Severity": { + "Product": 10, + "Normalized": 100 + }, + "Resources": [{ + "Type": "IpAddress", + "Id": issue['ip_address'] + }], + "RecordState": "ACTIVE", + } + + finding["Title"] = "Machines are vulnerable to 'Shellshock'" + finding["Description"] = "Update your Bash to a ShellShock-patched version." + finding["Remediation"] = { + "Recommendation": { + "Text": "The machine {0} ({1}) is vulnerable to a ShellShock attack. The attack was made possible because the HTTP server running on TCP port {2} was vulnerable to a shell injection attack on the paths: {3}.".format(issue['machine'], issue['ip_address'], issue['port'], issue['paths']) + } + } + return finding + + @staticmethod + def _handle_smb_password_issue(issue): + finding = \ + { + "Severity": { + "Product": 1, + "Normalized": 100 + }, + "Resources": [{ + "Type": "IpAddress", + "Id": issue['ip_address'] + }], + "RecordState": "ACTIVE", + } + + finding["Title"] = "Machines are accessible using passwords supplied by the user during the Monkey's configuration." + finding["Description"] = "Change {0}'s password to a complex one-use password that is not shared with other computers on the network." + finding["Remediation"] = { + "Recommendation": { + "Text": "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']) + } + } + return finding diff --git a/monkey/monkey_island/cc/resources/exporter.py b/monkey/monkey_island/cc/resources/exporter.py index 98f3e7662..1cf0c1b10 100644 --- a/monkey/monkey_island/cc/resources/exporter.py +++ b/monkey/monkey_island/cc/resources/exporter.py @@ -1,9 +1,9 @@ - class Exporter: def __init__(self): pass - def handle_report(self, report_json): + @staticmethod + def handle_report(report_json): raise NotImplementedError diff --git a/monkey/monkey_island/cc/resources/root.py b/monkey/monkey_island/cc/resources/root.py index 1d9141589..10e8f5170 100644 --- a/monkey/monkey_island/cc/resources/root.py +++ b/monkey/monkey_island/cc/resources/root.py @@ -65,5 +65,7 @@ class Root(flask_restful.Resource): if not infection_done: report_done = False else: + if is_any_exists: + ReportService.get_report() report_done = ReportService.is_report_generated() return dict(run_server=True, run_monkey=is_any_exists, infection_done=infection_done, report_done=report_done) diff --git a/monkey/monkey_island/cc/services/node.py b/monkey/monkey_island/cc/services/node.py index 072917974..1f9b68ebe 100644 --- a/monkey/monkey_island/cc/services/node.py +++ b/monkey/monkey_island/cc/services/node.py @@ -294,6 +294,10 @@ class NodeService: def is_monkey_finished_running(): return NodeService.is_any_monkey_exists() and not NodeService.is_any_monkey_alive() + @staticmethod + def get_latest_modified_monkey(): + return mongo.db.monkey.find({}).sort('modifytime', -1).limit(1) + @staticmethod def add_credentials_to_monkey(monkey_id, creds): mongo.db.monkey.update( diff --git a/monkey/monkey_island/cc/services/report.py b/monkey/monkey_island/cc/services/report.py index d8f9b9b96..b9fdf89e7 100644 --- a/monkey/monkey_island/cc/services/report.py +++ b/monkey/monkey_island/cc/services/report.py @@ -8,6 +8,8 @@ from enum import Enum from six import text_type from cc.database import mongo +from cc.environment.environment import load_env_from_file, AWS +from cc.resources.aws_exporter import AWSExporter from cc.services.config import ConfigService from cc.services.edge import EdgeService from cc.services.node import NodeService @@ -123,9 +125,9 @@ class ReportService: 'label': node['label'], 'ip_addresses': node['ip_addresses'], 'accessible_from_nodes': - (x['hostname'] for x in + list((x['hostname'] for x in (NodeService.get_displayed_node_by_id(edge['from'], True) - for edge in EdgeService.get_displayed_edges_by_to(node['id'], True))), + for edge in EdgeService.get_displayed_edges_by_to(node['id'], True)))), 'services': node['services'] }) @@ -659,26 +661,19 @@ class ReportService: @staticmethod def is_report_generated(): - generated_report = mongo.db.report.find_one({'name': 'generated_report'}) + generated_report = mongo.db.report.find_one({}) if generated_report is None: return False - return generated_report['value'] + return True @staticmethod - def set_report_generated(): - mongo.db.report.update( - {'name': 'generated_report'}, - {'$set': {'value': True}}, - upsert=True) - logger.info("Report marked as generated.") - - @staticmethod - def get_report(): + def generate_report(): domain_issues = ReportService.get_domain_issues() issues = ReportService.get_issues() config_users = ReportService.get_config_users() config_passwords = ReportService.get_config_passwords() cross_segment_issues = ReportService.get_cross_segment_issues() + monkey_latest_modify_time = list(NodeService.get_latest_modified_monkey())[0]['modifytime'] report = \ { @@ -710,17 +705,50 @@ class ReportService: { 'issues': issues, 'domain_issues': domain_issues + }, + 'meta': + { + 'latest_monkey_modifytime': monkey_latest_modify_time } } - - finished_run = NodeService.is_monkey_finished_running() - if finished_run: - ReportService.set_report_generated() + ReportService.export_to_exporters(report) + mongo.db.report.drop() + mongo.db.report.insert_one(report) return report + @staticmethod + def is_latest_report_exists(): + latest_report_doc = mongo.db.report.find_one({}, {'meta.latest_monkey_modifytime': 1}) + + if latest_report_doc: + report_latest_modifytime = latest_report_doc['meta']['latest_monkey_modifytime'] + latest_monkey_modifytime = NodeService.get_latest_modified_monkey()[0]['modifytime'] + return report_latest_modifytime == latest_monkey_modifytime + + return False + + @staticmethod + def get_report(): + if ReportService.is_latest_report_exists(): + return mongo.db.report.find_one() + return ReportService.generate_report() + @staticmethod def did_exploit_type_succeed(exploit_type): return mongo.db.edge.count( {'exploits': {'$elemMatch': {'exploiter': exploit_type, 'result': True}}}, limit=1) > 0 + + @staticmethod + def get_active_exporters(): + # This function should be in another module in charge of building a list of active exporters + exporters_list = [] + if load_env_from_file() == AWS: + exporters_list.append(AWSExporter) + return exporters_list + + @staticmethod + def export_to_exporters(report): + for exporter in ReportService.get_active_exporters(): + exporter.handle_report(report) diff --git a/monkey/monkey_island/requirements.txt b/monkey/monkey_island/requirements.txt index 29c364c9f..f094df947 100644 --- a/monkey/monkey_island/requirements.txt +++ b/monkey/monkey_island/requirements.txt @@ -14,3 +14,4 @@ netifaces ipaddress enum34 PyCrypto +boto3 \ No newline at end of file From d21558e81a978f8e00d37873039967ebc36a6948 Mon Sep 17 00:00:00 2001 From: "maor.rayzin" Date: Sun, 25 Nov 2018 14:17:20 +0200 Subject: [PATCH 03/46] * encrypted config --- monkey/monkey_island/cc/services/config.py | 37 ++++++++++++++++++- .../ui/src/components/pages/ConfigurePage.js | 2 +- 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/monkey/monkey_island/cc/services/config.py b/monkey/monkey_island/cc/services/config.py index 64b359f61..33223a6e7 100644 --- a/monkey/monkey_island/cc/services/config.py +++ b/monkey/monkey_island/cc/services/config.py @@ -862,7 +862,37 @@ SCHEMA = { } } } - } + }, + 'island_configuration': { + 'title': 'Island Configuration', + 'type': 'object', + 'properties': + { + 'aws_config': + { + 'title': 'AWS Configuration', + 'type': 'object', + 'properties': + { + 'iam_role_id': + { + 'title': 'IAM role ID', + 'type': 'string' + }, + 'aws_access_key': + { + 'title': 'AWS access key ID', + 'type': 'string' + }, + 'aws_secret_access_key': + { + 'title': 'AWS Secret Access Key', + 'type': 'string' + } + } + } + } + } }, "options": { "collapsed": True @@ -874,7 +904,10 @@ ENCRYPTED_CONFIG_ARRAYS = \ ['basic', 'credentials', 'exploit_password_list'], ['internal', 'exploits', 'exploit_lm_hash_list'], ['internal', 'exploits', 'exploit_ntlm_hash_list'], - ['internal', 'exploits', 'exploit_ssh_keys'] + ['internal', 'exploits', 'exploit_ssh_keys'], + ['island_configuration', 'aws_config', 'iam_role_id'], + ['island_configuration', 'aws_config', 'aws_access_key'], + ['island_configuration', 'aws_config', 'aws_secret_access_key'], ] diff --git a/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js b/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js index a97447df0..7e08170e2 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js @@ -10,7 +10,7 @@ class ConfigurePageComponent extends AuthComponent { this.currentSection = 'basic'; this.currentFormData = {}; - this.sectionsOrder = ['basic', 'basic_network', 'monkey', 'cnc', 'network', 'exploits', 'internal']; + this.sectionsOrder = ['basic', 'basic_network', 'monkey', 'cnc', 'network', 'exploits', 'internal', 'monkey_island']; // set schema from server this.state = { From 2dfbc1645082feff66ea56d76fab6bcfd536b27b Mon Sep 17 00:00:00 2001 From: "maor.rayzin" Date: Mon, 26 Nov 2018 11:48:43 +0200 Subject: [PATCH 04/46] * Added aws creds keys to configuration * Added boto session creation using credentials * Added a flag in the get_config function to separate island configuration values from monkey ones. * --- .../cc/resources/aws_exporter.py | 21 ++++++- monkey/monkey_island/cc/services/config.py | 63 +++++++++---------- .../ui/src/components/pages/ConfigurePage.js | 2 +- 3 files changed, 48 insertions(+), 38 deletions(-) diff --git a/monkey/monkey_island/cc/resources/aws_exporter.py b/monkey/monkey_island/cc/resources/aws_exporter.py index 363114948..f8501c30c 100644 --- a/monkey/monkey_island/cc/resources/aws_exporter.py +++ b/monkey/monkey_island/cc/resources/aws_exporter.py @@ -4,9 +4,13 @@ from datetime import datetime import boto3 from cc.resources.exporter import Exporter +from cc.services.config import ConfigService logger = logging.getLogger(__name__) +AWS_CRED_CONFIG_KEYS = [['cnc', 'aws_config', 'aws_access_key_id'], + ['cnc', 'aws_config', 'aws_secret_access_key']] + class AWSExporter(Exporter): @@ -19,12 +23,21 @@ class AWSExporter(Exporter): for issue in issues_list[machine]: findings_list.append(AWSExporter._prepare_finding(issue)) - if not AWSExporter._send_findings(findings_list): + if not AWSExporter._send_findings(findings_list, AWSExporter._get_aws_keys()): logger.error('Exporting findings to aws failed') return False return True + @staticmethod + def _get_aws_keys(): + creds_dict = {} + for key in AWS_CRED_CONFIG_KEYS: + creds_dict[key[2]] = ConfigService.get_config_value(key) + + return creds_dict + + @staticmethod def merge_two_dicts(x, y): z = x.copy() # start with x's keys and values @@ -60,9 +73,11 @@ class AWSExporter(Exporter): return AWSExporter.merge_two_dicts(finding, findings_dict[issue['type']](issue)) @staticmethod - def _send_findings(findings_list): + def _send_findings(findings_list, creds_dict): - securityhub = boto3.client('securityhub') + securityhub = boto3.client('securityhub', + aws_access_key_id=creds_dict.get('aws_access_key_id', ''), + aws_secret_access_key=creds_dict.get('aws_secret_access_key', '')) import_response = securityhub.batch_import_findings(Findings=findings_list) print import_response if import_response['ResponseMetadata']['HTTPStatusCode'] == 200: diff --git a/monkey/monkey_island/cc/services/config.py b/monkey/monkey_island/cc/services/config.py index 33223a6e7..b5ef28f65 100644 --- a/monkey/monkey_island/cc/services/config.py +++ b/monkey/monkey_island/cc/services/config.py @@ -639,6 +639,28 @@ SCHEMA = { "description": "The current command server the monkey is communicating with" } } + }, + 'aws_config': { + 'title': 'AWS Configuration', + 'type': 'object', + 'description': 'These credentials will be used in order to export the monkey\'s findings to the AWS Security Hub.', + 'properties': { + 'iam_role_id': { + 'title': 'IAM role ID', + 'type': 'string', + 'description': '' + }, + 'aws_access_key_id': { + 'title': 'AWS access key ID', + 'type': 'string', + 'description': 'Your AWS public access key ID, can be found in the IAM user interface in the AWS console.' + }, + 'aws_secret_access_key': { + 'title': 'AWS secret access key', + 'type': 'string', + 'description': 'Your AWS secret access key id, you can get this after creating a public access key in the console.' + } + } } } }, @@ -863,36 +885,6 @@ SCHEMA = { } } }, - 'island_configuration': { - 'title': 'Island Configuration', - 'type': 'object', - 'properties': - { - 'aws_config': - { - 'title': 'AWS Configuration', - 'type': 'object', - 'properties': - { - 'iam_role_id': - { - 'title': 'IAM role ID', - 'type': 'string' - }, - 'aws_access_key': - { - 'title': 'AWS access key ID', - 'type': 'string' - }, - 'aws_secret_access_key': - { - 'title': 'AWS Secret Access Key', - 'type': 'string' - } - } - } - } - } }, "options": { "collapsed": True @@ -905,9 +897,9 @@ ENCRYPTED_CONFIG_ARRAYS = \ ['internal', 'exploits', 'exploit_lm_hash_list'], ['internal', 'exploits', 'exploit_ntlm_hash_list'], ['internal', 'exploits', 'exploit_ssh_keys'], - ['island_configuration', 'aws_config', 'iam_role_id'], - ['island_configuration', 'aws_config', 'aws_access_key'], - ['island_configuration', 'aws_config', 'aws_secret_access_key'], + # ['cnc', 'aws_config', 'iam_role_id'], + # ['cnc', 'aws_config', 'aws_access_key_id'], + # ['cnc', 'aws_config', 'aws_secret_access_key'], ] @@ -918,11 +910,12 @@ class ConfigService: pass @staticmethod - def get_config(is_initial_config=False, should_decrypt=True): + def get_config(is_initial_config=False, should_decrypt=True, is_island=False): """ Gets the entire global config. :param is_initial_config: If True, the initial config will be returned instead of the current config. :param should_decrypt: If True, all config values which are set as encrypted will be decrypted. + :param is_island: If True, will include island specific configuration parameters. :return: The entire global config. """ config = mongo.db.config.find_one({'name': 'initial' if is_initial_config else 'newconfig'}) or {} @@ -930,6 +923,8 @@ class ConfigService: config.pop(field, None) if should_decrypt and len(config) > 0: ConfigService.decrypt_config(config) + if not is_island: + config['cnc'].pop('aws_config', None) return config @staticmethod diff --git a/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js b/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js index 7e08170e2..a97447df0 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js @@ -10,7 +10,7 @@ class ConfigurePageComponent extends AuthComponent { this.currentSection = 'basic'; this.currentFormData = {}; - this.sectionsOrder = ['basic', 'basic_network', 'monkey', 'cnc', 'network', 'exploits', 'internal', 'monkey_island']; + this.sectionsOrder = ['basic', 'basic_network', 'monkey', 'cnc', 'network', 'exploits', 'internal']; // set schema from server this.state = { From 30a6d7542fc26e1f7eda497c5803b2f07142ed78 Mon Sep 17 00:00:00 2001 From: "maor.rayzin" Date: Mon, 26 Nov 2018 12:12:24 +0200 Subject: [PATCH 05/46] * deleted a line --- monkey/monkey_island/cc/resources/aws_exporter.py | 1 - 1 file changed, 1 deletion(-) diff --git a/monkey/monkey_island/cc/resources/aws_exporter.py b/monkey/monkey_island/cc/resources/aws_exporter.py index f8501c30c..6295f28f3 100644 --- a/monkey/monkey_island/cc/resources/aws_exporter.py +++ b/monkey/monkey_island/cc/resources/aws_exporter.py @@ -37,7 +37,6 @@ class AWSExporter(Exporter): return creds_dict - @staticmethod def merge_two_dicts(x, y): z = x.copy() # start with x's keys and values From a79c60e9bc2344c8cf4034abca733f2d25af98eb Mon Sep 17 00:00:00 2001 From: "maor.rayzin" Date: Mon, 26 Nov 2018 12:59:06 +0200 Subject: [PATCH 06/46] * added instance ID to each issue in an aws machine * changed findings resource to ec2 instance id instead of IP --- .../cc/resources/aws_exporter.py | 36 +++++++++---------- .../monkey_island/cc/resources/telemetry.py | 2 ++ monkey/monkey_island/cc/services/report.py | 7 ++++ 3 files changed, 27 insertions(+), 18 deletions(-) diff --git a/monkey/monkey_island/cc/resources/aws_exporter.py b/monkey/monkey_island/cc/resources/aws_exporter.py index 6295f28f3..3f138e688 100644 --- a/monkey/monkey_island/cc/resources/aws_exporter.py +++ b/monkey/monkey_island/cc/resources/aws_exporter.py @@ -93,8 +93,8 @@ class AWSExporter(Exporter): "Normalized": 100 }, "Resources": [{ - "Type": "IpAddress", - "Id": issue['dest'] + "Type": "AwsEc2Instance", + "Id": issue['aws_instance_id'] }], "RecordState": "ACTIVE", } @@ -118,8 +118,8 @@ class AWSExporter(Exporter): "Normalized": 100 }, "Resources": [{ - "Type": "IpAddress", - "Id": str(issue['ip_address']) + "Type": "AwsEc2Instance", + "Id": issue['aws_instance_id'] }], "RecordState": "ACTIVE", } @@ -143,8 +143,8 @@ class AWSExporter(Exporter): "Normalized": 100 }, "Resources": [{ - "Type": "IpAddress", - "Id": issue['ip_address'] + "Type": "AwsEc2Instance", + "Id": issue['aws_instance_id'] }], "RecordState": "ACTIVE", } @@ -167,8 +167,8 @@ class AWSExporter(Exporter): "Normalized": 100 }, "Resources": [{ - "Type": "IpAddress", - "Id": issue['ip_address'] + "Type": "AwsEc2Instance", + "Id": issue['aws_instance_id'] }], "RecordState": "ACTIVE", } @@ -191,8 +191,8 @@ class AWSExporter(Exporter): "Normalized": 100 }, "Resources": [{ - "Type": "IpAddress", - "Id": issue['ip_address'] + "Type": "AwsEc2Instance", + "Id": issue['aws_instance_id'] }], "RecordState": "ACTIVE", } @@ -215,8 +215,8 @@ class AWSExporter(Exporter): "Normalized": 100 }, "Resources": [{ - "Type": "IpAddress", - "Id": issue['networks'][0][:-2] + "Type": "AwsEc2Instance", + "Id": issue['aws_instance_id'] }], "RecordState": "ACTIVE", } @@ -243,8 +243,8 @@ class AWSExporter(Exporter): "Normalized": 100 }, "Resources": [{ - "Type": "IpAddress", - "Id": '10.0.0.1' + "Type": "AwsEc2Instance", + "Id": issue['aws_instance_id'] }], "RecordState": "ACTIVE", } @@ -267,8 +267,8 @@ class AWSExporter(Exporter): "Normalized": 100 }, "Resources": [{ - "Type": "IpAddress", - "Id": issue['ip_address'] + "Type": "AwsEc2Instance", + "Id": issue['aws_instance_id'] }], "RecordState": "ACTIVE", } @@ -291,8 +291,8 @@ class AWSExporter(Exporter): "Normalized": 100 }, "Resources": [{ - "Type": "IpAddress", - "Id": issue['ip_address'] + "Type": "AwsEc2Instance", + "Id": issue['aws_instance_id'] }], "RecordState": "ACTIVE", } diff --git a/monkey/monkey_island/cc/resources/telemetry.py b/monkey/monkey_island/cc/resources/telemetry.py index 0db3b0eb4..6fc8f06f8 100644 --- a/monkey/monkey_island/cc/resources/telemetry.py +++ b/monkey/monkey_island/cc/resources/telemetry.py @@ -191,6 +191,8 @@ class Telemetry(flask_restful.Resource): if 'wmi' in telemetry_json['data']: wmi_handler = WMIHandler(monkey_id, telemetry_json['data']['wmi'], users_secrets) wmi_handler.process_and_handle_wmi_info() + if 'aws' in telemetry_json['data']: + mongo.db.monkey.insert({'aws_instance_id': telemetry_json['data']['instance-id']}) @staticmethod def add_ip_to_ssh_keys(ip, ssh_info): diff --git a/monkey/monkey_island/cc/services/report.py b/monkey/monkey_island/cc/services/report.py index b9fdf89e7..7f4864e60 100644 --- a/monkey/monkey_island/cc/services/report.py +++ b/monkey/monkey_island/cc/services/report.py @@ -548,6 +548,10 @@ class ReportService: logger.info('Domain issues generated for reporting') return domain_issues_dict + @staticmethod + def get_machine_aws_instance_id(hostname): + return str(mongo.db.monkey.find({'hostname': hostname}, {'aws_instance_id': 1})) + @staticmethod def get_issues(): ISSUE_GENERATORS = [ @@ -564,8 +568,11 @@ class ReportService: for issue in issues: if issue.get('is_local', True): machine = issue.get('machine').upper() + aws_instance_id = ReportService.get_machine_aws_instance_id(issue.get('machine')) if machine not in issues_dict: issues_dict[machine] = [] + if aws_instance_id: + issue['aws_instance_id'] = aws_instance_id issues_dict[machine].append(issue) logger.info('Issues generated for reporting') return issues_dict From 4cc85448d7d8c7769b9a4ae4b3dab14335b04ef2 Mon Sep 17 00:00:00 2001 From: "maor.rayzin" Date: Mon, 26 Nov 2018 14:01:46 +0200 Subject: [PATCH 07/46] * add instance id to domain issues too --- monkey/monkey_island/cc/services/report.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/monkey/monkey_island/cc/services/report.py b/monkey/monkey_island/cc/services/report.py index 7f4864e60..a75fdb7dd 100644 --- a/monkey/monkey_island/cc/services/report.py +++ b/monkey/monkey_island/cc/services/report.py @@ -542,8 +542,11 @@ class ReportService: for issue in issues: if not issue.get('is_local', True): machine = issue.get('machine').upper() + aws_instance_id = ReportService.get_machine_aws_instance_id(issue.get('machine')) if machine not in domain_issues_dict: domain_issues_dict[machine] = [] + if aws_instance_id: + issue['aws_instance_id'] = aws_instance_id domain_issues_dict[machine].append(issue) logger.info('Domain issues generated for reporting') return domain_issues_dict From 984a64561e305e3c27aea9d5a801371f500647ea Mon Sep 17 00:00:00 2001 From: "maor.rayzin" Date: Mon, 26 Nov 2018 15:04:25 +0200 Subject: [PATCH 08/46] * a small fixup --- monkey/monkey_island/cc/resources/telemetry.py | 2 +- monkey/monkey_island/cc/services/config.py | 6 +++--- monkey/monkey_island/cc/services/report.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/monkey/monkey_island/cc/resources/telemetry.py b/monkey/monkey_island/cc/resources/telemetry.py index 6fc8f06f8..ab911a119 100644 --- a/monkey/monkey_island/cc/resources/telemetry.py +++ b/monkey/monkey_island/cc/resources/telemetry.py @@ -192,7 +192,7 @@ class Telemetry(flask_restful.Resource): wmi_handler = WMIHandler(monkey_id, telemetry_json['data']['wmi'], users_secrets) wmi_handler.process_and_handle_wmi_info() if 'aws' in telemetry_json['data']: - mongo.db.monkey.insert({'aws_instance_id': telemetry_json['data']['instance-id']}) + mongo.db.monkey.update_one({'_id': monkey_id}, {'aws_instance_id': telemetry_json['data']['instance-id']}) @staticmethod def add_ip_to_ssh_keys(ip, ssh_info): diff --git a/monkey/monkey_island/cc/services/config.py b/monkey/monkey_island/cc/services/config.py index b5ef28f65..52bafa36f 100644 --- a/monkey/monkey_island/cc/services/config.py +++ b/monkey/monkey_island/cc/services/config.py @@ -645,10 +645,10 @@ SCHEMA = { 'type': 'object', 'description': 'These credentials will be used in order to export the monkey\'s findings to the AWS Security Hub.', 'properties': { - 'iam_role_id': { - 'title': 'IAM role ID', + 'aws_account_id': { + 'title': 'AWS account ID', 'type': 'string', - 'description': '' + 'description': 'Your AWS account ID that is subscribed to security hub feeds' }, 'aws_access_key_id': { 'title': 'AWS access key ID', diff --git a/monkey/monkey_island/cc/services/report.py b/monkey/monkey_island/cc/services/report.py index a75fdb7dd..a002235a0 100644 --- a/monkey/monkey_island/cc/services/report.py +++ b/monkey/monkey_island/cc/services/report.py @@ -553,7 +553,7 @@ class ReportService: @staticmethod def get_machine_aws_instance_id(hostname): - return str(mongo.db.monkey.find({'hostname': hostname}, {'aws_instance_id': 1})) + return str(list(mongo.db.monkey.find({'hostname': hostname}, {'aws_instance_id': 1}))[0]['aws_instance_id']) @staticmethod def get_issues(): @@ -754,7 +754,7 @@ class ReportService: def get_active_exporters(): # This function should be in another module in charge of building a list of active exporters exporters_list = [] - if load_env_from_file() == AWS: + if str(load_env_from_file()) == 'standard': exporters_list.append(AWSExporter) return exporters_list From 8eca2ca1e91e60ab2c955342848862e82717b11a Mon Sep 17 00:00:00 2001 From: "maor.rayzin" Date: Tue, 27 Nov 2018 10:28:41 +0200 Subject: [PATCH 09/46] * Exceptions handling for sending findings --- monkey/monkey_island/cc/resources/aws_exporter.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/monkey/monkey_island/cc/resources/aws_exporter.py b/monkey/monkey_island/cc/resources/aws_exporter.py index 3f138e688..c2082629c 100644 --- a/monkey/monkey_island/cc/resources/aws_exporter.py +++ b/monkey/monkey_island/cc/resources/aws_exporter.py @@ -77,11 +77,15 @@ class AWSExporter(Exporter): securityhub = boto3.client('securityhub', aws_access_key_id=creds_dict.get('aws_access_key_id', ''), aws_secret_access_key=creds_dict.get('aws_secret_access_key', '')) - import_response = securityhub.batch_import_findings(Findings=findings_list) - print import_response - if import_response['ResponseMetadata']['HTTPStatusCode'] == 200: - return True - else: + try: + import_response = securityhub.batch_import_findings(Findings=findings_list) + print import_response + if import_response['ResponseMetadata']['HTTPStatusCode'] == 200: + return True + else: + return False + except Exception as e: + logger.error('AWS security hub findings failed to send.') return False @staticmethod From c47572cd532bcd55cc5b4b111c5a13882f174b18 Mon Sep 17 00:00:00 2001 From: "maor.rayzin" Date: Tue, 27 Nov 2018 11:08:43 +0200 Subject: [PATCH 10/46] * Added another configuration endpoint for the island specific fields --- monkey/monkey_island/cc/app.py | 2 ++ .../cc/resources/island_configuration.py | 24 +++++++++++++++++++ monkey/monkey_island/cc/services/config.py | 19 ++++++++------- .../ui/src/components/pages/ConfigurePage.js | 2 +- 4 files changed, 37 insertions(+), 10 deletions(-) create mode 100644 monkey/monkey_island/cc/resources/island_configuration.py diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index a9682cc90..5bb94b611 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -18,6 +18,7 @@ from cc.resources.log import Log from cc.resources.island_logs import IslandLog from cc.resources.monkey import Monkey from cc.resources.monkey_configuration import MonkeyConfiguration +from cc.resources.island_configuration import IslandConfiguration from cc.resources.monkey_download import MonkeyDownload from cc.resources.netmap import NetMap from cc.resources.node import Node @@ -104,6 +105,7 @@ def init_app(mongo_url): api.add_resource(ClientRun, '/api/client-monkey', '/api/client-monkey/') api.add_resource(Telemetry, '/api/telemetry', '/api/telemetry/', '/api/telemetry/') api.add_resource(MonkeyConfiguration, '/api/configuration', '/api/configuration/') + api.add_resource(IslandConfiguration, '/api/configuration/island', '/api/configuration/island/') api.add_resource(MonkeyDownload, '/api/monkey/download', '/api/monkey/download/', '/api/monkey/download/') api.add_resource(NetMap, '/api/netmap', '/api/netmap/') diff --git a/monkey/monkey_island/cc/resources/island_configuration.py b/monkey/monkey_island/cc/resources/island_configuration.py new file mode 100644 index 000000000..57fda34fe --- /dev/null +++ b/monkey/monkey_island/cc/resources/island_configuration.py @@ -0,0 +1,24 @@ +import json + +import flask_restful +from flask import request, jsonify, abort + +from cc.auth import jwt_required +from cc.services.config import ConfigService + + +class IslandConfiguration(flask_restful.Resource): + @jwt_required() + def get(self): + return jsonify(schema=ConfigService.get_config_schema(), + configuration=ConfigService.get_config(False, True, True)) + + @jwt_required() + def post(self): + config_json = json.loads(request.data) + if 'reset' in config_json: + ConfigService.reset_config() + else: + if not ConfigService.update_config(config_json, should_encrypt=True): + abort(400) + return self.get() diff --git a/monkey/monkey_island/cc/services/config.py b/monkey/monkey_island/cc/services/config.py index 1fb26cb1c..2058a61dd 100644 --- a/monkey/monkey_island/cc/services/config.py +++ b/monkey/monkey_island/cc/services/config.py @@ -648,17 +648,20 @@ SCHEMA = { 'aws_account_id': { 'title': 'AWS account ID', 'type': 'string', - 'description': 'Your AWS account ID that is subscribed to security hub feeds' + 'description': 'Your AWS account ID that is subscribed to security hub feeds', + 'default': " " }, 'aws_access_key_id': { 'title': 'AWS access key ID', 'type': 'string', - 'description': 'Your AWS public access key ID, can be found in the IAM user interface in the AWS console.' + 'description': 'Your AWS public access key ID, can be found in the IAM user interface in the AWS console.', + 'default': " " }, 'aws_secret_access_key': { 'title': 'AWS secret access key', 'type': 'string', - 'description': 'Your AWS secret access key id, you can get this after creating a public access key in the console.' + 'description': 'Your AWS secret access key id, you can get this after creating a public access key in the console.', + 'default': " " } } } @@ -897,16 +900,14 @@ ENCRYPTED_CONFIG_ARRAYS = \ ['basic', 'credentials', 'exploit_password_list'], ['internal', 'exploits', 'exploit_lm_hash_list'], ['internal', 'exploits', 'exploit_ntlm_hash_list'], - ['internal', 'exploits', 'exploit_ssh_keys'], - # ['cnc', 'aws_config', 'iam_role_id'], - # ['cnc', 'aws_config', 'aws_access_key_id'], - # ['cnc', 'aws_config', 'aws_secret_access_key'], + ['internal', 'exploits', 'exploit_ssh_keys'] ] # This should be used for config values of string type ENCRYPTED_CONFIG_STRINGS = \ [ - + ['cnc', 'aws_config', 'aws_access_key_id'], + ['cnc', 'aws_config', 'aws_secret_access_key'] ] @@ -931,7 +932,7 @@ class ConfigService: if should_decrypt and len(config) > 0: ConfigService.decrypt_config(config) if not is_island: - config['cnc'].pop('aws_config', None) + config.get('cnc', {}).pop('aws_config', None) return config @staticmethod diff --git a/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js b/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js index a97447df0..6cc7e009a 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js @@ -24,7 +24,7 @@ class ConfigurePageComponent extends AuthComponent { } componentDidMount() { - this.authFetch('/api/configuration') + this.authFetch('/api/configuration/island') .then(res => res.json()) .then(res => { let sections = []; From 673605b72181b7cc2611cd72dd30012a394fcb18 Mon Sep 17 00:00:00 2001 From: "maor.rayzin" Date: Tue, 27 Nov 2018 14:13:50 +0200 Subject: [PATCH 11/46] * Added aws region getter * Moved productARN to server_config.json file --- monkey/monkey_island/cc/environment/aws.py | 4 ++++ monkey/monkey_island/cc/environment/environment.py | 9 ++++++--- monkey/monkey_island/cc/resources/aws_exporter.py | 7 +++++-- monkey/monkey_island/cc/server_config.json | 5 ++++- 4 files changed, 19 insertions(+), 6 deletions(-) diff --git a/monkey/monkey_island/cc/environment/aws.py b/monkey/monkey_island/cc/environment/aws.py index b85a7d2e4..2a57f1cb7 100644 --- a/monkey/monkey_island/cc/environment/aws.py +++ b/monkey/monkey_island/cc/environment/aws.py @@ -15,6 +15,10 @@ class AwsEnvironment(Environment): def _get_instance_id(): return urllib2.urlopen('http://169.254.169.254/latest/meta-data/instance-id').read() + @staticmethod + def _get_region(): + return urllib2.urlopen('http://169.254.169.254/latest/meta-data/placement/availability-zone').read()[:-1] + def is_auth_enabled(self): return True diff --git a/monkey/monkey_island/cc/environment/environment.py b/monkey/monkey_island/cc/environment/environment.py index 70fc025c3..c15e70257 100644 --- a/monkey/monkey_island/cc/environment/environment.py +++ b/monkey/monkey_island/cc/environment/environment.py @@ -14,13 +14,16 @@ ENV_DICT = { } -def load_env_from_file(): +def load_server_configuration_from_file(): with open('monkey_island/cc/server_config.json', 'r') as f: config_content = f.read() - config_json = json.loads(config_content) - return config_json['server_config'] + return json.loads(config_content) +def load_env_from_file(): + config_json = load_server_configuration_from_file() + return config_json['server_config'] + try: __env_type = load_env_from_file() env = ENV_DICT[__env_type]() diff --git a/monkey/monkey_island/cc/resources/aws_exporter.py b/monkey/monkey_island/cc/resources/aws_exporter.py index c2082629c..480743026 100644 --- a/monkey/monkey_island/cc/resources/aws_exporter.py +++ b/monkey/monkey_island/cc/resources/aws_exporter.py @@ -5,6 +5,7 @@ import boto3 from cc.resources.exporter import Exporter from cc.services.config import ConfigService +from cc.environment.environment import load_server_configuration_from_file logger = logging.getLogger(__name__) @@ -57,10 +58,12 @@ class AWSExporter(Exporter): 'shared_passwords': AWSExporter._handle_shared_passwords_issue, } + product_arn = load_server_configuration_from_file()['aws'].get('sec_hub_product_arn', '') + finding = { "SchemaVersion": "2018-10-08", "Id": uuid.uuid4().hex, - "ProductArn": "arn:aws:securityhub:us-west-2:324264561773:product/aws/guardduty", + "ProductArn": product_arn, "GeneratorId": issue['type'], "AwsAccountId": "324264561773", "Types": [ @@ -308,4 +311,4 @@ class AWSExporter(Exporter): "Text": "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']) } } - return finding + return finding \ No newline at end of file diff --git a/monkey/monkey_island/cc/server_config.json b/monkey/monkey_island/cc/server_config.json index 2d1a5995b..4d8644cbb 100644 --- a/monkey/monkey_island/cc/server_config.json +++ b/monkey/monkey_island/cc/server_config.json @@ -1,3 +1,6 @@ { - "server_config": "standard" + "server_config": "standard", + "aws": { + "sec_hub_product_arn": "arn:aws:securityhub:us-west-2:324264561773:product/aws/guardduty" + } } \ No newline at end of file From c888ab7bc998c740953b65165435621f1753c9b4 Mon Sep 17 00:00:00 2001 From: "maor.rayzin" Date: Mon, 19 Nov 2018 15:40:16 +0200 Subject: [PATCH 12/46] adding the exporter father class and aws implement --- monkey/monkey_island/cc/resources/aws_exporter.py | 9 +++++++++ monkey/monkey_island/cc/resources/exporter.py | 9 +++++++++ 2 files changed, 18 insertions(+) create mode 100644 monkey/monkey_island/cc/resources/aws_exporter.py create mode 100644 monkey/monkey_island/cc/resources/exporter.py diff --git a/monkey/monkey_island/cc/resources/aws_exporter.py b/monkey/monkey_island/cc/resources/aws_exporter.py new file mode 100644 index 000000000..cca47d968 --- /dev/null +++ b/monkey/monkey_island/cc/resources/aws_exporter.py @@ -0,0 +1,9 @@ +from exporter import Exporter + +class AWSExporter(Exporter): + + def __init__(self): + Exporter.__init__(self) + + def handle_report(self, report_json): + pass \ No newline at end of file diff --git a/monkey/monkey_island/cc/resources/exporter.py b/monkey/monkey_island/cc/resources/exporter.py new file mode 100644 index 000000000..98f3e7662 --- /dev/null +++ b/monkey/monkey_island/cc/resources/exporter.py @@ -0,0 +1,9 @@ + + +class Exporter: + + def __init__(self): + pass + + def handle_report(self, report_json): + raise NotImplementedError From 148ee3f0f0afc0ec42aceebf41582ad64d1edb7a Mon Sep 17 00:00:00 2001 From: "maor.rayzin" Date: Sun, 25 Nov 2018 12:39:47 +0200 Subject: [PATCH 13/46] * Added env' config * Added exporters and aws exporter * changed report generation to be automatic on monkey death with support of on-demand report generation and mongo storage --- .../cc/environment/environment.py | 2 + .../cc/resources/aws_exporter.py | 294 +++++++++++++++++- monkey/monkey_island/cc/resources/exporter.py | 4 +- monkey/monkey_island/cc/resources/root.py | 2 + monkey/monkey_island/cc/services/node.py | 4 + monkey/monkey_island/cc/services/report.py | 62 +++- monkey/monkey_island/requirements.txt | 1 + 7 files changed, 345 insertions(+), 24 deletions(-) diff --git a/monkey/monkey_island/cc/environment/environment.py b/monkey/monkey_island/cc/environment/environment.py index 9e89208ef..70fc025c3 100644 --- a/monkey/monkey_island/cc/environment/environment.py +++ b/monkey/monkey_island/cc/environment/environment.py @@ -5,6 +5,8 @@ import aws logger = logging.getLogger(__name__) +AWS = 'aws' +STANDARD = 'standard' ENV_DICT = { 'standard': standard.StandardEnvironment, diff --git a/monkey/monkey_island/cc/resources/aws_exporter.py b/monkey/monkey_island/cc/resources/aws_exporter.py index cca47d968..363114948 100644 --- a/monkey/monkey_island/cc/resources/aws_exporter.py +++ b/monkey/monkey_island/cc/resources/aws_exporter.py @@ -1,9 +1,293 @@ -from exporter import Exporter +import logging +import uuid +from datetime import datetime +import boto3 + +from cc.resources.exporter import Exporter + +logger = logging.getLogger(__name__) + class AWSExporter(Exporter): - def __init__(self): - Exporter.__init__(self) + @staticmethod + def handle_report(report_json): - def handle_report(self, report_json): - pass \ No newline at end of file + findings_list = [] + issues_list = report_json['recommendations']['issues'] + for machine in issues_list: + for issue in issues_list[machine]: + findings_list.append(AWSExporter._prepare_finding(issue)) + + if not AWSExporter._send_findings(findings_list): + logger.error('Exporting findings to aws failed') + return False + + 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 + + @staticmethod + def _prepare_finding(issue): + findings_dict = { + 'island_cross_segment': AWSExporter._handle_island_cross_segment_issue, + 'ssh': AWSExporter._handle_ssh_issue, + 'shellshock': AWSExporter._handle_shellshock_issue, + 'tunnel': AWSExporter._handle_tunnel_issue, + 'elastic': AWSExporter._handle_elastic_issue, + 'smb_password': AWSExporter._handle_smb_password_issue, + 'smb_pth': AWSExporter._handle_smb_pth_issue, + 'sambacry': AWSExporter._handle_sambacry_issue, + 'shared_passwords': AWSExporter._handle_shared_passwords_issue, + } + + finding = { + "SchemaVersion": "2018-10-08", + "Id": uuid.uuid4().hex, + "ProductArn": "arn:aws:securityhub:us-west-2:324264561773:product/aws/guardduty", + "GeneratorId": issue['type'], + "AwsAccountId": "324264561773", + "Types": [ + "Software and Configuration Checks/Vulnerabilities/CVE" + ], + "CreatedAt": datetime.now().isoformat() + 'Z', + "UpdatedAt": datetime.now().isoformat() + 'Z', + } + return AWSExporter.merge_two_dicts(finding, findings_dict[issue['type']](issue)) + + @staticmethod + def _send_findings(findings_list): + + securityhub = boto3.client('securityhub') + import_response = securityhub.batch_import_findings(Findings=findings_list) + print import_response + if import_response['ResponseMetadata']['HTTPStatusCode'] == 200: + return True + else: + return False + + @staticmethod + def _handle_tunnel_issue(issue): + finding =\ + { + "Severity": { + "Product": 5, + "Normalized": 100 + }, + "Resources": [{ + "Type": "IpAddress", + "Id": issue['dest'] + }], + "RecordState": "ACTIVE", + } + + finding["Title"] = "Weak segmentation - Machines were able to communicate over unused ports." + finding["Description"] = "Use micro-segmentation policies to disable communication other than the required." + finding["Remediation"] = { + "Recommendation": { + "Text": "Machines are not locked down at port level. Network tunnel was set up from {0} to {1}" + .format(issue['machine'], issue['dest']) + } + } + return finding + + @staticmethod + def _handle_sambacry_issue(issue): + finding = \ + { + "Severity": { + "Product": 10, + "Normalized": 100 + }, + "Resources": [{ + "Type": "IpAddress", + "Id": str(issue['ip_address']) + }], + "RecordState": "ACTIVE", + } + + finding["Title"] = "Samba servers are vulnerable to 'SambaCry'" + finding["Description"] = "Change {0} password to a complex one-use password that is not shared with other computers on the network. Update your Samba server to 4.4.14 and up, 4.5.10 and up, or 4.6.4 and up."\ + .format(issue['username']) + finding["Remediation"] = { + "Recommendation": { + "Text": "The machine {0} ({1}) is vulnerable to a SambaCry attack. The Monkey authenticated over the SMB protocol with user {2} and its password, and used the SambaCry vulnerability.".format(issue['machine'], issue['ip_address'], issue['username']) + } + } + return finding + + @staticmethod + def _handle_smb_pth_issue(issue): + finding = \ + { + "Severity": { + "Product": 5, + "Normalized": 100 + }, + "Resources": [{ + "Type": "IpAddress", + "Id": issue['ip_address'] + }], + "RecordState": "ACTIVE", + } + + finding["Title"] = "Machines are accessible using passwords supplied by the user during the Monkey's configuration." + finding["Description"] = "Change {0}'s password to a complex one-use password that is not shared with other computers on the network.".format(issue['username']) + finding["Remediation"] = { + "Recommendation": { + "Text": "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']) + } + } + return finding + + @staticmethod + def _handle_ssh_issue(issue): + finding = \ + { + "Severity": { + "Product": 1, + "Normalized": 100 + }, + "Resources": [{ + "Type": "IpAddress", + "Id": issue['ip_address'] + }], + "RecordState": "ACTIVE", + } + + finding["Title"] = "Machines are accessible using SSH passwords supplied by the user during the Monkey's configuration." + finding["Description"] = "Change {0}'s password to a complex one-use password that is not shared with other computers on the network.".format(issue['username']) + finding["Remediation"] = { + "Recommendation": { + "Text": "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']) + } + } + return finding + + @staticmethod + def _handle_elastic_issue(issue): + finding = \ + { + "Severity": { + "Product": 10, + "Normalized": 100 + }, + "Resources": [{ + "Type": "IpAddress", + "Id": issue['ip_address'] + }], + "RecordState": "ACTIVE", + } + + finding["Title"] = "Elasticsearch servers are vulnerable to CVE-2015-1427" + finding["Description"] = "Update your Elastic Search server to version 1.4.3 and up." + finding["Remediation"] = { + "Recommendation": { + "Text": "The machine {0}({1}) is vulnerable to an Elastic Groovy attack. The attack was made possible because the Elastic Search server was not patched against CVE-2015-1427.".format(issue['machine'], issue['ip_address']) + } + } + return finding + + @staticmethod + def _handle_island_cross_segment_issue(issue): + finding = \ + { + "Severity": { + "Product": 1, + "Normalized": 100 + }, + "Resources": [{ + "Type": "IpAddress", + "Id": issue['networks'][0][:-2] + }], + "RecordState": "ACTIVE", + } + + finding["Title"] = "Weak segmentation - Machines from different segments are able to communicate." + finding["Description"] = "egment your network and make sure there is no communication between machines from different segments." + finding["Remediation"] = { + "Recommendation": { + "Text": "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']) + } + } + return finding + + @staticmethod + def _handle_shared_passwords_issue(issue): + finding = \ + { + "Severity": { + "Product": 1, + "Normalized": 100 + }, + "Resources": [{ + "Type": "IpAddress", + "Id": '10.0.0.1' + }], + "RecordState": "ACTIVE", + } + + finding["Title"] = "Multiple users have the same password" + finding["Description"] = "Some users are sharing passwords, this should be fixed by changing passwords." + finding["Remediation"] = { + "Recommendation": { + "Text": "These users are sharing access password: {0}.".format(issue['shared_with']) + } + } + return finding + + @staticmethod + def _handle_shellshock_issue(issue): + finding = \ + { + "Severity": { + "Product": 10, + "Normalized": 100 + }, + "Resources": [{ + "Type": "IpAddress", + "Id": issue['ip_address'] + }], + "RecordState": "ACTIVE", + } + + finding["Title"] = "Machines are vulnerable to 'Shellshock'" + finding["Description"] = "Update your Bash to a ShellShock-patched version." + finding["Remediation"] = { + "Recommendation": { + "Text": "The machine {0} ({1}) is vulnerable to a ShellShock attack. The attack was made possible because the HTTP server running on TCP port {2} was vulnerable to a shell injection attack on the paths: {3}.".format(issue['machine'], issue['ip_address'], issue['port'], issue['paths']) + } + } + return finding + + @staticmethod + def _handle_smb_password_issue(issue): + finding = \ + { + "Severity": { + "Product": 1, + "Normalized": 100 + }, + "Resources": [{ + "Type": "IpAddress", + "Id": issue['ip_address'] + }], + "RecordState": "ACTIVE", + } + + finding["Title"] = "Machines are accessible using passwords supplied by the user during the Monkey's configuration." + finding["Description"] = "Change {0}'s password to a complex one-use password that is not shared with other computers on the network." + finding["Remediation"] = { + "Recommendation": { + "Text": "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']) + } + } + return finding diff --git a/monkey/monkey_island/cc/resources/exporter.py b/monkey/monkey_island/cc/resources/exporter.py index 98f3e7662..1cf0c1b10 100644 --- a/monkey/monkey_island/cc/resources/exporter.py +++ b/monkey/monkey_island/cc/resources/exporter.py @@ -1,9 +1,9 @@ - class Exporter: def __init__(self): pass - def handle_report(self, report_json): + @staticmethod + def handle_report(report_json): raise NotImplementedError diff --git a/monkey/monkey_island/cc/resources/root.py b/monkey/monkey_island/cc/resources/root.py index 1d9141589..10e8f5170 100644 --- a/monkey/monkey_island/cc/resources/root.py +++ b/monkey/monkey_island/cc/resources/root.py @@ -65,5 +65,7 @@ class Root(flask_restful.Resource): if not infection_done: report_done = False else: + if is_any_exists: + ReportService.get_report() report_done = ReportService.is_report_generated() return dict(run_server=True, run_monkey=is_any_exists, infection_done=infection_done, report_done=report_done) diff --git a/monkey/monkey_island/cc/services/node.py b/monkey/monkey_island/cc/services/node.py index 072917974..1f9b68ebe 100644 --- a/monkey/monkey_island/cc/services/node.py +++ b/monkey/monkey_island/cc/services/node.py @@ -294,6 +294,10 @@ class NodeService: def is_monkey_finished_running(): return NodeService.is_any_monkey_exists() and not NodeService.is_any_monkey_alive() + @staticmethod + def get_latest_modified_monkey(): + return mongo.db.monkey.find({}).sort('modifytime', -1).limit(1) + @staticmethod def add_credentials_to_monkey(monkey_id, creds): mongo.db.monkey.update( diff --git a/monkey/monkey_island/cc/services/report.py b/monkey/monkey_island/cc/services/report.py index 38bf6fe79..1320facfe 100644 --- a/monkey/monkey_island/cc/services/report.py +++ b/monkey/monkey_island/cc/services/report.py @@ -8,6 +8,8 @@ from enum import Enum from six import text_type from cc.database import mongo +from cc.environment.environment import load_env_from_file, AWS +from cc.resources.aws_exporter import AWSExporter from cc.services.config import ConfigService from cc.services.edge import EdgeService from cc.services.node import NodeService @@ -123,9 +125,9 @@ class ReportService: 'label': node['label'], 'ip_addresses': node['ip_addresses'], 'accessible_from_nodes': - (x['hostname'] for x in + list((x['hostname'] for x in (NodeService.get_displayed_node_by_id(edge['from'], True) - for edge in EdgeService.get_displayed_edges_by_to(node['id'], True))), + for edge in EdgeService.get_displayed_edges_by_to(node['id'], True)))), 'services': node['services'] }) @@ -659,26 +661,19 @@ class ReportService: @staticmethod def is_report_generated(): - generated_report = mongo.db.report.find_one({'name': 'generated_report'}) + generated_report = mongo.db.report.find_one({}) if generated_report is None: return False - return generated_report['value'] + return True @staticmethod - def set_report_generated(): - mongo.db.report.update( - {'name': 'generated_report'}, - {'$set': {'value': True}}, - upsert=True) - logger.info("Report marked as generated.") - - @staticmethod - def get_report(): + def generate_report(): domain_issues = ReportService.get_domain_issues() issues = ReportService.get_issues() config_users = ReportService.get_config_users() config_passwords = ReportService.get_config_passwords() cross_segment_issues = ReportService.get_cross_segment_issues() + monkey_latest_modify_time = list(NodeService.get_latest_modified_monkey())[0]['modifytime'] report = \ { @@ -710,17 +705,50 @@ class ReportService: { 'issues': issues, 'domain_issues': domain_issues + }, + 'meta': + { + 'latest_monkey_modifytime': monkey_latest_modify_time } } - - finished_run = NodeService.is_monkey_finished_running() - if finished_run: - ReportService.set_report_generated() + ReportService.export_to_exporters(report) + mongo.db.report.drop() + mongo.db.report.insert_one(report) return report + @staticmethod + def is_latest_report_exists(): + latest_report_doc = mongo.db.report.find_one({}, {'meta.latest_monkey_modifytime': 1}) + + if latest_report_doc: + report_latest_modifytime = latest_report_doc['meta']['latest_monkey_modifytime'] + latest_monkey_modifytime = NodeService.get_latest_modified_monkey()[0]['modifytime'] + return report_latest_modifytime == latest_monkey_modifytime + + return False + + @staticmethod + def get_report(): + if ReportService.is_latest_report_exists(): + return mongo.db.report.find_one() + return ReportService.generate_report() + @staticmethod def did_exploit_type_succeed(exploit_type): return mongo.db.edge.count( {'exploits': {'$elemMatch': {'exploiter': exploit_type, 'result': True}}}, limit=1) > 0 + + @staticmethod + def get_active_exporters(): + # This function should be in another module in charge of building a list of active exporters + exporters_list = [] + if load_env_from_file() == AWS: + exporters_list.append(AWSExporter) + return exporters_list + + @staticmethod + def export_to_exporters(report): + for exporter in ReportService.get_active_exporters(): + exporter.handle_report(report) diff --git a/monkey/monkey_island/requirements.txt b/monkey/monkey_island/requirements.txt index 29c364c9f..f094df947 100644 --- a/monkey/monkey_island/requirements.txt +++ b/monkey/monkey_island/requirements.txt @@ -14,3 +14,4 @@ netifaces ipaddress enum34 PyCrypto +boto3 \ No newline at end of file From dd5bbdec35166f7db840848786c63f9465808102 Mon Sep 17 00:00:00 2001 From: "maor.rayzin" Date: Sun, 25 Nov 2018 14:17:20 +0200 Subject: [PATCH 14/46] * encrypted config --- monkey/monkey_island/cc/services/config.py | 37 ++++++++++++++++++- .../ui/src/components/pages/ConfigurePage.js | 2 +- 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/monkey/monkey_island/cc/services/config.py b/monkey/monkey_island/cc/services/config.py index 1b2966026..3c61a89a3 100644 --- a/monkey/monkey_island/cc/services/config.py +++ b/monkey/monkey_island/cc/services/config.py @@ -862,7 +862,37 @@ SCHEMA = { } } } - } + }, + 'island_configuration': { + 'title': 'Island Configuration', + 'type': 'object', + 'properties': + { + 'aws_config': + { + 'title': 'AWS Configuration', + 'type': 'object', + 'properties': + { + 'iam_role_id': + { + 'title': 'IAM role ID', + 'type': 'string' + }, + 'aws_access_key': + { + 'title': 'AWS access key ID', + 'type': 'string' + }, + 'aws_secret_access_key': + { + 'title': 'AWS Secret Access Key', + 'type': 'string' + } + } + } + } + } }, "options": { "collapsed": True @@ -875,7 +905,10 @@ ENCRYPTED_CONFIG_ARRAYS = \ ['basic', 'credentials', 'exploit_password_list'], ['internal', 'exploits', 'exploit_lm_hash_list'], ['internal', 'exploits', 'exploit_ntlm_hash_list'], - ['internal', 'exploits', 'exploit_ssh_keys'] + ['internal', 'exploits', 'exploit_ssh_keys'], + ['island_configuration', 'aws_config', 'iam_role_id'], + ['island_configuration', 'aws_config', 'aws_access_key'], + ['island_configuration', 'aws_config', 'aws_secret_access_key'], ] # This should be used for config values of string type diff --git a/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js b/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js index a97447df0..7e08170e2 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js @@ -10,7 +10,7 @@ class ConfigurePageComponent extends AuthComponent { this.currentSection = 'basic'; this.currentFormData = {}; - this.sectionsOrder = ['basic', 'basic_network', 'monkey', 'cnc', 'network', 'exploits', 'internal']; + this.sectionsOrder = ['basic', 'basic_network', 'monkey', 'cnc', 'network', 'exploits', 'internal', 'monkey_island']; // set schema from server this.state = { From f8f7421c4724e0bfcf8147107d26919f8e8f58d0 Mon Sep 17 00:00:00 2001 From: "maor.rayzin" Date: Mon, 26 Nov 2018 11:48:43 +0200 Subject: [PATCH 15/46] * Added aws creds keys to configuration * Added boto session creation using credentials * Added a flag in the get_config function to separate island configuration values from monkey ones. --- .../cc/resources/aws_exporter.py | 20 +++++- monkey/monkey_island/cc/services/config.py | 63 +++++++++---------- .../ui/src/components/pages/ConfigurePage.js | 2 +- 3 files changed, 47 insertions(+), 38 deletions(-) diff --git a/monkey/monkey_island/cc/resources/aws_exporter.py b/monkey/monkey_island/cc/resources/aws_exporter.py index 363114948..6295f28f3 100644 --- a/monkey/monkey_island/cc/resources/aws_exporter.py +++ b/monkey/monkey_island/cc/resources/aws_exporter.py @@ -4,9 +4,13 @@ from datetime import datetime import boto3 from cc.resources.exporter import Exporter +from cc.services.config import ConfigService logger = logging.getLogger(__name__) +AWS_CRED_CONFIG_KEYS = [['cnc', 'aws_config', 'aws_access_key_id'], + ['cnc', 'aws_config', 'aws_secret_access_key']] + class AWSExporter(Exporter): @@ -19,12 +23,20 @@ class AWSExporter(Exporter): for issue in issues_list[machine]: findings_list.append(AWSExporter._prepare_finding(issue)) - if not AWSExporter._send_findings(findings_list): + if not AWSExporter._send_findings(findings_list, AWSExporter._get_aws_keys()): logger.error('Exporting findings to aws failed') return False return True + @staticmethod + def _get_aws_keys(): + creds_dict = {} + for key in AWS_CRED_CONFIG_KEYS: + creds_dict[key[2]] = ConfigService.get_config_value(key) + + return creds_dict + @staticmethod def merge_two_dicts(x, y): z = x.copy() # start with x's keys and values @@ -60,9 +72,11 @@ class AWSExporter(Exporter): return AWSExporter.merge_two_dicts(finding, findings_dict[issue['type']](issue)) @staticmethod - def _send_findings(findings_list): + def _send_findings(findings_list, creds_dict): - securityhub = boto3.client('securityhub') + securityhub = boto3.client('securityhub', + aws_access_key_id=creds_dict.get('aws_access_key_id', ''), + aws_secret_access_key=creds_dict.get('aws_secret_access_key', '')) import_response = securityhub.batch_import_findings(Findings=findings_list) print import_response if import_response['ResponseMetadata']['HTTPStatusCode'] == 200: diff --git a/monkey/monkey_island/cc/services/config.py b/monkey/monkey_island/cc/services/config.py index 3c61a89a3..6255a0656 100644 --- a/monkey/monkey_island/cc/services/config.py +++ b/monkey/monkey_island/cc/services/config.py @@ -639,6 +639,28 @@ SCHEMA = { "description": "The current command server the monkey is communicating with" } } + }, + 'aws_config': { + 'title': 'AWS Configuration', + 'type': 'object', + 'description': 'These credentials will be used in order to export the monkey\'s findings to the AWS Security Hub.', + 'properties': { + 'iam_role_id': { + 'title': 'IAM role ID', + 'type': 'string', + 'description': '' + }, + 'aws_access_key_id': { + 'title': 'AWS access key ID', + 'type': 'string', + 'description': 'Your AWS public access key ID, can be found in the IAM user interface in the AWS console.' + }, + 'aws_secret_access_key': { + 'title': 'AWS secret access key', + 'type': 'string', + 'description': 'Your AWS secret access key id, you can get this after creating a public access key in the console.' + } + } } } }, @@ -863,36 +885,6 @@ SCHEMA = { } } }, - 'island_configuration': { - 'title': 'Island Configuration', - 'type': 'object', - 'properties': - { - 'aws_config': - { - 'title': 'AWS Configuration', - 'type': 'object', - 'properties': - { - 'iam_role_id': - { - 'title': 'IAM role ID', - 'type': 'string' - }, - 'aws_access_key': - { - 'title': 'AWS access key ID', - 'type': 'string' - }, - 'aws_secret_access_key': - { - 'title': 'AWS Secret Access Key', - 'type': 'string' - } - } - } - } - } }, "options": { "collapsed": True @@ -906,9 +898,9 @@ ENCRYPTED_CONFIG_ARRAYS = \ ['internal', 'exploits', 'exploit_lm_hash_list'], ['internal', 'exploits', 'exploit_ntlm_hash_list'], ['internal', 'exploits', 'exploit_ssh_keys'], - ['island_configuration', 'aws_config', 'iam_role_id'], - ['island_configuration', 'aws_config', 'aws_access_key'], - ['island_configuration', 'aws_config', 'aws_secret_access_key'], + # ['cnc', 'aws_config', 'iam_role_id'], + # ['cnc', 'aws_config', 'aws_access_key_id'], + # ['cnc', 'aws_config', 'aws_secret_access_key'], ] # This should be used for config values of string type @@ -925,11 +917,12 @@ class ConfigService: pass @staticmethod - def get_config(is_initial_config=False, should_decrypt=True): + def get_config(is_initial_config=False, should_decrypt=True, is_island=False): """ Gets the entire global config. :param is_initial_config: If True, the initial config will be returned instead of the current config. :param should_decrypt: If True, all config values which are set as encrypted will be decrypted. + :param is_island: If True, will include island specific configuration parameters. :return: The entire global config. """ config = mongo.db.config.find_one({'name': 'initial' if is_initial_config else 'newconfig'}) or {} @@ -937,6 +930,8 @@ class ConfigService: config.pop(field, None) if should_decrypt and len(config) > 0: ConfigService.decrypt_config(config) + if not is_island: + config['cnc'].pop('aws_config', None) return config @staticmethod diff --git a/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js b/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js index 7e08170e2..a97447df0 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js @@ -10,7 +10,7 @@ class ConfigurePageComponent extends AuthComponent { this.currentSection = 'basic'; this.currentFormData = {}; - this.sectionsOrder = ['basic', 'basic_network', 'monkey', 'cnc', 'network', 'exploits', 'internal', 'monkey_island']; + this.sectionsOrder = ['basic', 'basic_network', 'monkey', 'cnc', 'network', 'exploits', 'internal']; // set schema from server this.state = { From 1912a274222c9175cdc520f91dc1ea047ebaed09 Mon Sep 17 00:00:00 2001 From: "maor.rayzin" Date: Mon, 26 Nov 2018 12:59:06 +0200 Subject: [PATCH 16/46] * added instance ID to each issue in an aws machine * changed findings resource to ec2 instance id instead of IP --- .../cc/resources/aws_exporter.py | 36 +++++++++---------- .../monkey_island/cc/resources/telemetry.py | 2 ++ monkey/monkey_island/cc/services/report.py | 7 ++++ 3 files changed, 27 insertions(+), 18 deletions(-) diff --git a/monkey/monkey_island/cc/resources/aws_exporter.py b/monkey/monkey_island/cc/resources/aws_exporter.py index 6295f28f3..3f138e688 100644 --- a/monkey/monkey_island/cc/resources/aws_exporter.py +++ b/monkey/monkey_island/cc/resources/aws_exporter.py @@ -93,8 +93,8 @@ class AWSExporter(Exporter): "Normalized": 100 }, "Resources": [{ - "Type": "IpAddress", - "Id": issue['dest'] + "Type": "AwsEc2Instance", + "Id": issue['aws_instance_id'] }], "RecordState": "ACTIVE", } @@ -118,8 +118,8 @@ class AWSExporter(Exporter): "Normalized": 100 }, "Resources": [{ - "Type": "IpAddress", - "Id": str(issue['ip_address']) + "Type": "AwsEc2Instance", + "Id": issue['aws_instance_id'] }], "RecordState": "ACTIVE", } @@ -143,8 +143,8 @@ class AWSExporter(Exporter): "Normalized": 100 }, "Resources": [{ - "Type": "IpAddress", - "Id": issue['ip_address'] + "Type": "AwsEc2Instance", + "Id": issue['aws_instance_id'] }], "RecordState": "ACTIVE", } @@ -167,8 +167,8 @@ class AWSExporter(Exporter): "Normalized": 100 }, "Resources": [{ - "Type": "IpAddress", - "Id": issue['ip_address'] + "Type": "AwsEc2Instance", + "Id": issue['aws_instance_id'] }], "RecordState": "ACTIVE", } @@ -191,8 +191,8 @@ class AWSExporter(Exporter): "Normalized": 100 }, "Resources": [{ - "Type": "IpAddress", - "Id": issue['ip_address'] + "Type": "AwsEc2Instance", + "Id": issue['aws_instance_id'] }], "RecordState": "ACTIVE", } @@ -215,8 +215,8 @@ class AWSExporter(Exporter): "Normalized": 100 }, "Resources": [{ - "Type": "IpAddress", - "Id": issue['networks'][0][:-2] + "Type": "AwsEc2Instance", + "Id": issue['aws_instance_id'] }], "RecordState": "ACTIVE", } @@ -243,8 +243,8 @@ class AWSExporter(Exporter): "Normalized": 100 }, "Resources": [{ - "Type": "IpAddress", - "Id": '10.0.0.1' + "Type": "AwsEc2Instance", + "Id": issue['aws_instance_id'] }], "RecordState": "ACTIVE", } @@ -267,8 +267,8 @@ class AWSExporter(Exporter): "Normalized": 100 }, "Resources": [{ - "Type": "IpAddress", - "Id": issue['ip_address'] + "Type": "AwsEc2Instance", + "Id": issue['aws_instance_id'] }], "RecordState": "ACTIVE", } @@ -291,8 +291,8 @@ class AWSExporter(Exporter): "Normalized": 100 }, "Resources": [{ - "Type": "IpAddress", - "Id": issue['ip_address'] + "Type": "AwsEc2Instance", + "Id": issue['aws_instance_id'] }], "RecordState": "ACTIVE", } diff --git a/monkey/monkey_island/cc/resources/telemetry.py b/monkey/monkey_island/cc/resources/telemetry.py index 0db3b0eb4..6fc8f06f8 100644 --- a/monkey/monkey_island/cc/resources/telemetry.py +++ b/monkey/monkey_island/cc/resources/telemetry.py @@ -191,6 +191,8 @@ class Telemetry(flask_restful.Resource): if 'wmi' in telemetry_json['data']: wmi_handler = WMIHandler(monkey_id, telemetry_json['data']['wmi'], users_secrets) wmi_handler.process_and_handle_wmi_info() + if 'aws' in telemetry_json['data']: + mongo.db.monkey.insert({'aws_instance_id': telemetry_json['data']['instance-id']}) @staticmethod def add_ip_to_ssh_keys(ip, ssh_info): diff --git a/monkey/monkey_island/cc/services/report.py b/monkey/monkey_island/cc/services/report.py index 1320facfe..428d5ac70 100644 --- a/monkey/monkey_island/cc/services/report.py +++ b/monkey/monkey_island/cc/services/report.py @@ -548,6 +548,10 @@ class ReportService: logger.info('Domain issues generated for reporting') return domain_issues_dict + @staticmethod + def get_machine_aws_instance_id(hostname): + return str(mongo.db.monkey.find({'hostname': hostname}, {'aws_instance_id': 1})) + @staticmethod def get_issues(): ISSUE_GENERATORS = [ @@ -564,8 +568,11 @@ class ReportService: for issue in issues: if issue.get('is_local', True): machine = issue.get('machine').upper() + aws_instance_id = ReportService.get_machine_aws_instance_id(issue.get('machine')) if machine not in issues_dict: issues_dict[machine] = [] + if aws_instance_id: + issue['aws_instance_id'] = aws_instance_id issues_dict[machine].append(issue) logger.info('Issues generated for reporting') return issues_dict From a00bfc17e3149f89cd4d2543ad4596592186b746 Mon Sep 17 00:00:00 2001 From: "maor.rayzin" Date: Mon, 26 Nov 2018 14:01:46 +0200 Subject: [PATCH 17/46] * add instance id to domain issues too --- monkey/monkey_island/cc/services/report.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/monkey/monkey_island/cc/services/report.py b/monkey/monkey_island/cc/services/report.py index 428d5ac70..2d290886e 100644 --- a/monkey/monkey_island/cc/services/report.py +++ b/monkey/monkey_island/cc/services/report.py @@ -542,8 +542,11 @@ class ReportService: for issue in issues: if not issue.get('is_local', True): machine = issue.get('machine').upper() + aws_instance_id = ReportService.get_machine_aws_instance_id(issue.get('machine')) if machine not in domain_issues_dict: domain_issues_dict[machine] = [] + if aws_instance_id: + issue['aws_instance_id'] = aws_instance_id domain_issues_dict[machine].append(issue) logger.info('Domain issues generated for reporting') return domain_issues_dict From f506eb3dd14aa1a57a347b38cc940c04ad2ad98a Mon Sep 17 00:00:00 2001 From: "maor.rayzin" Date: Mon, 26 Nov 2018 15:04:25 +0200 Subject: [PATCH 18/46] * a small fixup --- monkey/monkey_island/cc/resources/telemetry.py | 2 +- monkey/monkey_island/cc/services/config.py | 6 +++--- monkey/monkey_island/cc/services/report.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/monkey/monkey_island/cc/resources/telemetry.py b/monkey/monkey_island/cc/resources/telemetry.py index 6fc8f06f8..ab911a119 100644 --- a/monkey/monkey_island/cc/resources/telemetry.py +++ b/monkey/monkey_island/cc/resources/telemetry.py @@ -192,7 +192,7 @@ class Telemetry(flask_restful.Resource): wmi_handler = WMIHandler(monkey_id, telemetry_json['data']['wmi'], users_secrets) wmi_handler.process_and_handle_wmi_info() if 'aws' in telemetry_json['data']: - mongo.db.monkey.insert({'aws_instance_id': telemetry_json['data']['instance-id']}) + mongo.db.monkey.update_one({'_id': monkey_id}, {'aws_instance_id': telemetry_json['data']['instance-id']}) @staticmethod def add_ip_to_ssh_keys(ip, ssh_info): diff --git a/monkey/monkey_island/cc/services/config.py b/monkey/monkey_island/cc/services/config.py index 6255a0656..1fb26cb1c 100644 --- a/monkey/monkey_island/cc/services/config.py +++ b/monkey/monkey_island/cc/services/config.py @@ -645,10 +645,10 @@ SCHEMA = { 'type': 'object', 'description': 'These credentials will be used in order to export the monkey\'s findings to the AWS Security Hub.', 'properties': { - 'iam_role_id': { - 'title': 'IAM role ID', + 'aws_account_id': { + 'title': 'AWS account ID', 'type': 'string', - 'description': '' + 'description': 'Your AWS account ID that is subscribed to security hub feeds' }, 'aws_access_key_id': { 'title': 'AWS access key ID', diff --git a/monkey/monkey_island/cc/services/report.py b/monkey/monkey_island/cc/services/report.py index 2d290886e..961bb1195 100644 --- a/monkey/monkey_island/cc/services/report.py +++ b/monkey/monkey_island/cc/services/report.py @@ -553,7 +553,7 @@ class ReportService: @staticmethod def get_machine_aws_instance_id(hostname): - return str(mongo.db.monkey.find({'hostname': hostname}, {'aws_instance_id': 1})) + return str(list(mongo.db.monkey.find({'hostname': hostname}, {'aws_instance_id': 1}))[0]['aws_instance_id']) @staticmethod def get_issues(): @@ -754,7 +754,7 @@ class ReportService: def get_active_exporters(): # This function should be in another module in charge of building a list of active exporters exporters_list = [] - if load_env_from_file() == AWS: + if str(load_env_from_file()) == 'standard': exporters_list.append(AWSExporter) return exporters_list From 90554f63cb119dc0a498273c4de41db0b1fff127 Mon Sep 17 00:00:00 2001 From: "maor.rayzin" Date: Tue, 27 Nov 2018 10:28:41 +0200 Subject: [PATCH 19/46] * Exceptions handling for sending findings --- monkey/monkey_island/cc/resources/aws_exporter.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/monkey/monkey_island/cc/resources/aws_exporter.py b/monkey/monkey_island/cc/resources/aws_exporter.py index 3f138e688..c2082629c 100644 --- a/monkey/monkey_island/cc/resources/aws_exporter.py +++ b/monkey/monkey_island/cc/resources/aws_exporter.py @@ -77,11 +77,15 @@ class AWSExporter(Exporter): securityhub = boto3.client('securityhub', aws_access_key_id=creds_dict.get('aws_access_key_id', ''), aws_secret_access_key=creds_dict.get('aws_secret_access_key', '')) - import_response = securityhub.batch_import_findings(Findings=findings_list) - print import_response - if import_response['ResponseMetadata']['HTTPStatusCode'] == 200: - return True - else: + try: + import_response = securityhub.batch_import_findings(Findings=findings_list) + print import_response + if import_response['ResponseMetadata']['HTTPStatusCode'] == 200: + return True + else: + return False + except Exception as e: + logger.error('AWS security hub findings failed to send.') return False @staticmethod From a42d621340363fc7b6e8873f177dc18cf457b28d Mon Sep 17 00:00:00 2001 From: "maor.rayzin" Date: Tue, 27 Nov 2018 11:08:43 +0200 Subject: [PATCH 20/46] * Added another configuration endpoint for the island specific fields --- monkey/monkey_island/cc/app.py | 2 ++ .../cc/resources/island_configuration.py | 24 +++++++++++++++++++ monkey/monkey_island/cc/services/config.py | 19 ++++++++------- .../ui/src/components/pages/ConfigurePage.js | 2 +- 4 files changed, 37 insertions(+), 10 deletions(-) create mode 100644 monkey/monkey_island/cc/resources/island_configuration.py diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index a9682cc90..5bb94b611 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -18,6 +18,7 @@ from cc.resources.log import Log from cc.resources.island_logs import IslandLog from cc.resources.monkey import Monkey from cc.resources.monkey_configuration import MonkeyConfiguration +from cc.resources.island_configuration import IslandConfiguration from cc.resources.monkey_download import MonkeyDownload from cc.resources.netmap import NetMap from cc.resources.node import Node @@ -104,6 +105,7 @@ def init_app(mongo_url): api.add_resource(ClientRun, '/api/client-monkey', '/api/client-monkey/') api.add_resource(Telemetry, '/api/telemetry', '/api/telemetry/', '/api/telemetry/') api.add_resource(MonkeyConfiguration, '/api/configuration', '/api/configuration/') + api.add_resource(IslandConfiguration, '/api/configuration/island', '/api/configuration/island/') api.add_resource(MonkeyDownload, '/api/monkey/download', '/api/monkey/download/', '/api/monkey/download/') api.add_resource(NetMap, '/api/netmap', '/api/netmap/') diff --git a/monkey/monkey_island/cc/resources/island_configuration.py b/monkey/monkey_island/cc/resources/island_configuration.py new file mode 100644 index 000000000..57fda34fe --- /dev/null +++ b/monkey/monkey_island/cc/resources/island_configuration.py @@ -0,0 +1,24 @@ +import json + +import flask_restful +from flask import request, jsonify, abort + +from cc.auth import jwt_required +from cc.services.config import ConfigService + + +class IslandConfiguration(flask_restful.Resource): + @jwt_required() + def get(self): + return jsonify(schema=ConfigService.get_config_schema(), + configuration=ConfigService.get_config(False, True, True)) + + @jwt_required() + def post(self): + config_json = json.loads(request.data) + if 'reset' in config_json: + ConfigService.reset_config() + else: + if not ConfigService.update_config(config_json, should_encrypt=True): + abort(400) + return self.get() diff --git a/monkey/monkey_island/cc/services/config.py b/monkey/monkey_island/cc/services/config.py index 1fb26cb1c..2058a61dd 100644 --- a/monkey/monkey_island/cc/services/config.py +++ b/monkey/monkey_island/cc/services/config.py @@ -648,17 +648,20 @@ SCHEMA = { 'aws_account_id': { 'title': 'AWS account ID', 'type': 'string', - 'description': 'Your AWS account ID that is subscribed to security hub feeds' + 'description': 'Your AWS account ID that is subscribed to security hub feeds', + 'default': " " }, 'aws_access_key_id': { 'title': 'AWS access key ID', 'type': 'string', - 'description': 'Your AWS public access key ID, can be found in the IAM user interface in the AWS console.' + 'description': 'Your AWS public access key ID, can be found in the IAM user interface in the AWS console.', + 'default': " " }, 'aws_secret_access_key': { 'title': 'AWS secret access key', 'type': 'string', - 'description': 'Your AWS secret access key id, you can get this after creating a public access key in the console.' + 'description': 'Your AWS secret access key id, you can get this after creating a public access key in the console.', + 'default': " " } } } @@ -897,16 +900,14 @@ ENCRYPTED_CONFIG_ARRAYS = \ ['basic', 'credentials', 'exploit_password_list'], ['internal', 'exploits', 'exploit_lm_hash_list'], ['internal', 'exploits', 'exploit_ntlm_hash_list'], - ['internal', 'exploits', 'exploit_ssh_keys'], - # ['cnc', 'aws_config', 'iam_role_id'], - # ['cnc', 'aws_config', 'aws_access_key_id'], - # ['cnc', 'aws_config', 'aws_secret_access_key'], + ['internal', 'exploits', 'exploit_ssh_keys'] ] # This should be used for config values of string type ENCRYPTED_CONFIG_STRINGS = \ [ - + ['cnc', 'aws_config', 'aws_access_key_id'], + ['cnc', 'aws_config', 'aws_secret_access_key'] ] @@ -931,7 +932,7 @@ class ConfigService: if should_decrypt and len(config) > 0: ConfigService.decrypt_config(config) if not is_island: - config['cnc'].pop('aws_config', None) + config.get('cnc', {}).pop('aws_config', None) return config @staticmethod diff --git a/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js b/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js index a97447df0..6cc7e009a 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js @@ -24,7 +24,7 @@ class ConfigurePageComponent extends AuthComponent { } componentDidMount() { - this.authFetch('/api/configuration') + this.authFetch('/api/configuration/island') .then(res => res.json()) .then(res => { let sections = []; From 8e6ab5b9f58e403692392b1f961a0a468c963a31 Mon Sep 17 00:00:00 2001 From: "maor.rayzin" Date: Tue, 27 Nov 2018 14:13:50 +0200 Subject: [PATCH 21/46] * Added aws region getter * Moved productARN to server_config.json file --- monkey/monkey_island/cc/environment/aws.py | 4 ++++ monkey/monkey_island/cc/environment/environment.py | 9 ++++++--- monkey/monkey_island/cc/resources/aws_exporter.py | 7 +++++-- monkey/monkey_island/cc/server_config.json | 5 ++++- 4 files changed, 19 insertions(+), 6 deletions(-) diff --git a/monkey/monkey_island/cc/environment/aws.py b/monkey/monkey_island/cc/environment/aws.py index 464d42323..e3c139e90 100644 --- a/monkey/monkey_island/cc/environment/aws.py +++ b/monkey/monkey_island/cc/environment/aws.py @@ -14,6 +14,10 @@ class AwsEnvironment(Environment): def _get_instance_id(): return AWS.get_instance_id() + @staticmethod + def _get_region(): + return urllib2.urlopen('http://169.254.169.254/latest/meta-data/placement/availability-zone').read()[:-1] + def is_auth_enabled(self): return True diff --git a/monkey/monkey_island/cc/environment/environment.py b/monkey/monkey_island/cc/environment/environment.py index 70fc025c3..c15e70257 100644 --- a/monkey/monkey_island/cc/environment/environment.py +++ b/monkey/monkey_island/cc/environment/environment.py @@ -14,13 +14,16 @@ ENV_DICT = { } -def load_env_from_file(): +def load_server_configuration_from_file(): with open('monkey_island/cc/server_config.json', 'r') as f: config_content = f.read() - config_json = json.loads(config_content) - return config_json['server_config'] + return json.loads(config_content) +def load_env_from_file(): + config_json = load_server_configuration_from_file() + return config_json['server_config'] + try: __env_type = load_env_from_file() env = ENV_DICT[__env_type]() diff --git a/monkey/monkey_island/cc/resources/aws_exporter.py b/monkey/monkey_island/cc/resources/aws_exporter.py index c2082629c..480743026 100644 --- a/monkey/monkey_island/cc/resources/aws_exporter.py +++ b/monkey/monkey_island/cc/resources/aws_exporter.py @@ -5,6 +5,7 @@ import boto3 from cc.resources.exporter import Exporter from cc.services.config import ConfigService +from cc.environment.environment import load_server_configuration_from_file logger = logging.getLogger(__name__) @@ -57,10 +58,12 @@ class AWSExporter(Exporter): 'shared_passwords': AWSExporter._handle_shared_passwords_issue, } + product_arn = load_server_configuration_from_file()['aws'].get('sec_hub_product_arn', '') + finding = { "SchemaVersion": "2018-10-08", "Id": uuid.uuid4().hex, - "ProductArn": "arn:aws:securityhub:us-west-2:324264561773:product/aws/guardduty", + "ProductArn": product_arn, "GeneratorId": issue['type'], "AwsAccountId": "324264561773", "Types": [ @@ -308,4 +311,4 @@ class AWSExporter(Exporter): "Text": "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']) } } - return finding + return finding \ No newline at end of file diff --git a/monkey/monkey_island/cc/server_config.json b/monkey/monkey_island/cc/server_config.json index 2d1a5995b..4d8644cbb 100644 --- a/monkey/monkey_island/cc/server_config.json +++ b/monkey/monkey_island/cc/server_config.json @@ -1,3 +1,6 @@ { - "server_config": "standard" + "server_config": "standard", + "aws": { + "sec_hub_product_arn": "arn:aws:securityhub:us-west-2:324264561773:product/aws/guardduty" + } } \ No newline at end of file From bf29cddf4d4921d7f492635654e0f79369709ba4 Mon Sep 17 00:00:00 2001 From: "maor.rayzin" Date: Tue, 27 Nov 2018 14:44:39 +0200 Subject: [PATCH 22/46] * Fixed the aws env class to not be static anymore after itay's change. * Added aws region getter --- monkey/common/cloud/aws.py | 4 ++++ monkey/monkey_island/cc/environment/aws.py | 14 +++++++------- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/monkey/common/cloud/aws.py b/monkey/common/cloud/aws.py index 53b0690f9..90267bca7 100644 --- a/monkey/common/cloud/aws.py +++ b/monkey/common/cloud/aws.py @@ -7,11 +7,15 @@ class AWS(object): def __init__(self): try: self.instance_id = urllib2.urlopen('http://169.254.169.254/latest/meta-data/instance-id').read() + self.region = urllib2.urlopen('http://169.254.169.254/latest/meta-data/placement/availability-zone').read()[:-1] except urllib2.URLError: self.instance_id = None def get_instance_id(self): return self.instance_id + def get_region(self): + return self.region + def is_aws_instance(self): return self.instance_id is not None diff --git a/monkey/monkey_island/cc/environment/aws.py b/monkey/monkey_island/cc/environment/aws.py index e3c139e90..a004a2540 100644 --- a/monkey/monkey_island/cc/environment/aws.py +++ b/monkey/monkey_island/cc/environment/aws.py @@ -8,15 +8,15 @@ __author__ = 'itay.mizeretz' class AwsEnvironment(Environment): def __init__(self): super(AwsEnvironment, self).__init__() - self._instance_id = AwsEnvironment._get_instance_id() + self.aws_info = AWS() + self._instance_id = self._get_instance_id() + self.region = self._get_region() - @staticmethod - def _get_instance_id(): - return AWS.get_instance_id() + def _get_instance_id(self): + return self.aws_info.get_instance_id() - @staticmethod - def _get_region(): - return urllib2.urlopen('http://169.254.169.254/latest/meta-data/placement/availability-zone').read()[:-1] + def _get_region(self): + return self.aws_info.get_region() def is_auth_enabled(self): return True From fb5ae63f0476d6e9134a639038664a8e4a26c49d Mon Sep 17 00:00:00 2001 From: "maor.rayzin" Date: Tue, 27 Nov 2018 14:45:44 +0200 Subject: [PATCH 23/46] * Fixed the aws env class to not be static anymore after itay's change. * Added aws region getter --- monkey/monkey_island/cc/environment/aws.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/monkey/monkey_island/cc/environment/aws.py b/monkey/monkey_island/cc/environment/aws.py index 2d62079e6..a004a2540 100644 --- a/monkey/monkey_island/cc/environment/aws.py +++ b/monkey/monkey_island/cc/environment/aws.py @@ -18,10 +18,6 @@ class AwsEnvironment(Environment): def _get_region(self): return self.aws_info.get_region() - @staticmethod - def _get_region(): - return urllib2.urlopen('http://169.254.169.254/latest/meta-data/placement/availability-zone').read()[:-1] - def is_auth_enabled(self): return True From 9e6b2b2d2664100efa5de13e02dda25d0a0aaec6 Mon Sep 17 00:00:00 2001 From: "maor.rayzin" Date: Tue, 27 Nov 2018 16:57:53 +0200 Subject: [PATCH 24/46] * Added missing findings * switched to using the aws account id from the island's configuration page --- .../cc/resources/aws_exporter.py | 524 ++++++++++++------ monkey/monkey_island/cc/services/config.py | 1 + 2 files changed, 355 insertions(+), 170 deletions(-) diff --git a/monkey/monkey_island/cc/resources/aws_exporter.py b/monkey/monkey_island/cc/resources/aws_exporter.py index 480743026..0d9b0a157 100644 --- a/monkey/monkey_island/cc/resources/aws_exporter.py +++ b/monkey/monkey_island/cc/resources/aws_exporter.py @@ -10,7 +10,8 @@ from cc.environment.environment import load_server_configuration_from_file logger = logging.getLogger(__name__) AWS_CRED_CONFIG_KEYS = [['cnc', 'aws_config', 'aws_access_key_id'], - ['cnc', 'aws_config', 'aws_secret_access_key']] + ['cnc', 'aws_config', 'aws_secret_access_key'], + ['cnc', 'aws_config', 'aws_account_id']] class AWSExporter(Exporter): @@ -34,7 +35,7 @@ class AWSExporter(Exporter): def _get_aws_keys(): creds_dict = {} for key in AWS_CRED_CONFIG_KEYS: - creds_dict[key[2]] = ConfigService.get_config_value(key) + creds_dict[key[2]] = str(ConfigService.get_config_value(key)) return creds_dict @@ -56,16 +57,28 @@ class AWSExporter(Exporter): 'smb_pth': AWSExporter._handle_smb_pth_issue, 'sambacry': AWSExporter._handle_sambacry_issue, 'shared_passwords': AWSExporter._handle_shared_passwords_issue, + 'wmi_password': AWSExporter._handle_wmi_password_issue, + 'wmi_pth': AWSExporter._handle_wmi_pth_issue, + 'ssh_key': AWSExporter._handle_ssh_key_issue, + 'rdp': AWSExporter._handle_rdp_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, + 'struts2': AWSExporter._handle_struts2_issue, + 'weblogic': AWSExporter._handle_weblogic_issue, + 'hadoop': AWSExporter._handle_hadoop_issue, + # azure and conficker are not relevant issues for an AWS env } product_arn = load_server_configuration_from_file()['aws'].get('sec_hub_product_arn', '') + account_id = AWSExporter._get_aws_keys().get('aws_account_id', '') finding = { "SchemaVersion": "2018-10-08", "Id": uuid.uuid4().hex, "ProductArn": product_arn, "GeneratorId": issue['type'], - "AwsAccountId": "324264561773", + "AwsAccountId": account_id, "Types": [ "Software and Configuration Checks/Vulnerabilities/CVE" ], @@ -93,222 +106,393 @@ class AWSExporter(Exporter): @staticmethod def _handle_tunnel_issue(issue): - finding =\ - { - "Severity": { - "Product": 5, - "Normalized": 100 - }, - "Resources": [{ - "Type": "AwsEc2Instance", - "Id": issue['aws_instance_id'] - }], - "RecordState": "ACTIVE", - } + finding = \ + {"Severity": { + "Product": 5, + "Normalized": 100 + }, "Resources": [{ + "Type": "AwsEc2Instance", + "Id": issue['aws_instance_id'] + }], "RecordState": "ACTIVE", + "Title": "Weak segmentation - Machines were able to communicate over unused ports.", + "Description": "Use micro-segmentation policies to disable communication other than the required.", + "Remediation": { + "Recommendation": { + "Text": "Machines are not locked down at port level. Network tunnel was set up from {0} to {1}" + .format(issue['machine'], issue['dest']) + } + }} - finding["Title"] = "Weak segmentation - Machines were able to communicate over unused ports." - finding["Description"] = "Use micro-segmentation policies to disable communication other than the required." - finding["Remediation"] = { - "Recommendation": { - "Text": "Machines are not locked down at port level. Network tunnel was set up from {0} to {1}" - .format(issue['machine'], issue['dest']) - } - } return finding @staticmethod def _handle_sambacry_issue(issue): finding = \ - { - "Severity": { - "Product": 10, - "Normalized": 100 - }, - "Resources": [{ - "Type": "AwsEc2Instance", - "Id": issue['aws_instance_id'] - }], - "RecordState": "ACTIVE", - } + {"Severity": { + "Product": 10, + "Normalized": 100 + }, "Resources": [{ + "Type": "AwsEc2Instance", + "Id": issue['aws_instance_id'] + }], "RecordState": "ACTIVE", "Title": "Samba servers are vulnerable to 'SambaCry'", + "Description": "Change {0} password to a complex one-use password that is not shared with other computers on the network. Update your Samba server to 4.4.14 and up, 4.5.10 and up, or 4.6.4 and up." \ + .format(issue['username']), "Remediation": { + "Recommendation": { + "Text": "The machine {0} ({1}) is vulnerable to a SambaCry attack. The Monkey authenticated over the SMB protocol with user {2} and its password, and used the SambaCry vulnerability.".format( + issue['machine'], issue['ip_address'], issue['username']) + } + }} - finding["Title"] = "Samba servers are vulnerable to 'SambaCry'" - finding["Description"] = "Change {0} password to a complex one-use password that is not shared with other computers on the network. Update your Samba server to 4.4.14 and up, 4.5.10 and up, or 4.6.4 and up."\ - .format(issue['username']) - finding["Remediation"] = { - "Recommendation": { - "Text": "The machine {0} ({1}) is vulnerable to a SambaCry attack. The Monkey authenticated over the SMB protocol with user {2} and its password, and used the SambaCry vulnerability.".format(issue['machine'], issue['ip_address'], issue['username']) - } - } return finding @staticmethod def _handle_smb_pth_issue(issue): finding = \ - { - "Severity": { - "Product": 5, - "Normalized": 100 - }, - "Resources": [{ - "Type": "AwsEc2Instance", - "Id": issue['aws_instance_id'] - }], - "RecordState": "ACTIVE", - } + {"Severity": { + "Product": 5, + "Normalized": 100 + }, "Resources": [{ + "Type": "AwsEc2Instance", + "Id": issue['aws_instance_id'] + }], "RecordState": "ACTIVE", + "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']), "Remediation": { + "Recommendation": { + "Text": "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']) + } + }} - finding["Title"] = "Machines are accessible using passwords supplied by the user during the Monkey's configuration." - finding["Description"] = "Change {0}'s password to a complex one-use password that is not shared with other computers on the network.".format(issue['username']) - finding["Remediation"] = { - "Recommendation": { - "Text": "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']) - } - } return finding @staticmethod def _handle_ssh_issue(issue): finding = \ - { - "Severity": { - "Product": 1, - "Normalized": 100 - }, - "Resources": [{ - "Type": "AwsEc2Instance", - "Id": issue['aws_instance_id'] - }], - "RecordState": "ACTIVE", - } + {"Severity": { + "Product": 1, + "Normalized": 100 + }, "Resources": [{ + "Type": "AwsEc2Instance", + "Id": issue['aws_instance_id'] + }], "RecordState": "ACTIVE", + "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']), "Remediation": { + "Recommendation": { + "Text": "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']) + } + }} + + return finding + + @staticmethod + def _handle_ssh_key_issue(issue): + finding = \ + {"Severity": { + "Product": 1, + "Normalized": 100 + }, "Resources": [{ + "Type": "AwsEc2Instance", + "Id": issue['aws_instance_id'] + }], "RecordState": "ACTIVE", + "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']), + "Remediation": { + "Recommendation": { + "Text": "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']) + } + }} - finding["Title"] = "Machines are accessible using SSH passwords supplied by the user during the Monkey's configuration." - finding["Description"] = "Change {0}'s password to a complex one-use password that is not shared with other computers on the network.".format(issue['username']) - finding["Remediation"] = { - "Recommendation": { - "Text": "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']) - } - } return finding @staticmethod def _handle_elastic_issue(issue): finding = \ - { - "Severity": { - "Product": 10, - "Normalized": 100 - }, - "Resources": [{ - "Type": "AwsEc2Instance", - "Id": issue['aws_instance_id'] - }], - "RecordState": "ACTIVE", - } + {"Severity": { + "Product": 10, + "Normalized": 100 + }, "Resources": [{ + "Type": "AwsEc2Instance", + "Id": issue['aws_instance_id'] + }], "RecordState": "ACTIVE", "Title": "Elasticsearch servers are vulnerable to CVE-2015-1427", + "Description": "Update your Elastic Search server to version 1.4.3 and up.", "Remediation": { + "Recommendation": { + "Text": "The machine {0}({1}) is vulnerable to an Elastic Groovy attack. The attack was made possible because the Elastic Search server was not patched against CVE-2015-1427.".format( + issue['machine'], issue['ip_address']) + } + }} - finding["Title"] = "Elasticsearch servers are vulnerable to CVE-2015-1427" - finding["Description"] = "Update your Elastic Search server to version 1.4.3 and up." - finding["Remediation"] = { - "Recommendation": { - "Text": "The machine {0}({1}) is vulnerable to an Elastic Groovy attack. The attack was made possible because the Elastic Search server was not patched against CVE-2015-1427.".format(issue['machine'], issue['ip_address']) - } - } return finding @staticmethod def _handle_island_cross_segment_issue(issue): finding = \ - { - "Severity": { - "Product": 1, - "Normalized": 100 - }, - "Resources": [{ - "Type": "AwsEc2Instance", - "Id": issue['aws_instance_id'] - }], - "RecordState": "ACTIVE", - } - - finding["Title"] = "Weak segmentation - Machines from different segments are able to communicate." - finding["Description"] = "egment your network and make sure there is no communication between machines from different segments." - finding["Remediation"] = { - "Recommendation": { - "Text": "The network can probably be segmented. A monkey instance on \ + {"Severity": { + "Product": 1, + "Normalized": 100 + }, "Resources": [{ + "Type": "AwsEc2Instance", + "Id": issue['aws_instance_id'] + }], "RecordState": "ACTIVE", + "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.", + "Remediation": { + "Recommendation": { + "Text": "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']) - } - } + } + }} + return finding @staticmethod def _handle_shared_passwords_issue(issue): finding = \ - { - "Severity": { - "Product": 1, - "Normalized": 100 - }, - "Resources": [{ - "Type": "AwsEc2Instance", - "Id": issue['aws_instance_id'] - }], - "RecordState": "ACTIVE", - } + {"Severity": { + "Product": 1, + "Normalized": 100 + }, "Resources": [{ + "Type": "AwsEc2Instance", + "Id": issue['aws_instance_id'] + }], "RecordState": "ACTIVE", "Title": "Multiple users have the same password", + "Description": "Some users are sharing passwords, this should be fixed by changing passwords.", + "Remediation": { + "Recommendation": { + "Text": "These users are sharing access password: {0}.".format(issue['shared_with']) + } + }} - finding["Title"] = "Multiple users have the same password" - finding["Description"] = "Some users are sharing passwords, this should be fixed by changing passwords." - finding["Remediation"] = { - "Recommendation": { - "Text": "These users are sharing access password: {0}.".format(issue['shared_with']) - } - } return finding @staticmethod def _handle_shellshock_issue(issue): finding = \ - { - "Severity": { - "Product": 10, - "Normalized": 100 - }, - "Resources": [{ - "Type": "AwsEc2Instance", - "Id": issue['aws_instance_id'] - }], - "RecordState": "ACTIVE", - } + {"Severity": { + "Product": 10, + "Normalized": 100 + }, "Resources": [{ + "Type": "AwsEc2Instance", + "Id": issue['aws_instance_id'] + }], "RecordState": "ACTIVE", "Title": "Machines are vulnerable to 'Shellshock'", + "Description": "Update your Bash to a ShellShock-patched version.", "Remediation": { + "Recommendation": { + "Text": "The machine {0} ({1}) is vulnerable to a ShellShock attack. " + "The attack was made possible because the HTTP server running on TCP port {2} was vulnerable to a shell injection attack on the paths: {3}.".format( + issue['machine'], issue['ip_address'], issue['port'], issue['paths']) + } + }} - finding["Title"] = "Machines are vulnerable to 'Shellshock'" - finding["Description"] = "Update your Bash to a ShellShock-patched version." - finding["Remediation"] = { - "Recommendation": { - "Text": "The machine {0} ({1}) is vulnerable to a ShellShock attack. The attack was made possible because the HTTP server running on TCP port {2} was vulnerable to a shell injection attack on the paths: {3}.".format(issue['machine'], issue['ip_address'], issue['port'], issue['paths']) - } - } return finding @staticmethod def _handle_smb_password_issue(issue): finding = \ - { - "Severity": { - "Product": 1, - "Normalized": 100 - }, - "Resources": [{ - "Type": "AwsEc2Instance", - "Id": issue['aws_instance_id'] - }], - "RecordState": "ACTIVE", - } + {"Severity": { + "Product": 1, + "Normalized": 100 + }, "Resources": [{ + "Type": "AwsEc2Instance", + "Id": issue['aws_instance_id'] + }], "RecordState": "ACTIVE", + "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']), "Remediation": { + "Recommendation": { + "Text": "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']) + } + }} - finding["Title"] = "Machines are accessible using passwords supplied by the user during the Monkey's configuration." - finding["Description"] = "Change {0}'s password to a complex one-use password that is not shared with other computers on the network." - finding["Remediation"] = { - "Recommendation": { - "Text": "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']) - } - } - return finding \ No newline at end of file + return finding + + @staticmethod + def _handle_wmi_password_issue(issue): + finding = \ + {"Severity": { + "Product": 1, + "Normalized": 100 + }, "Resources": [{ + "Type": "AwsEc2Instance", + "Id": issue['aws_instance_id'] + }], "RecordState": "ACTIVE", + "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.", + "Remediation": { + "Recommendation": { + "Text": "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']) + } + }} + + return finding + + @staticmethod + def _handle_wmi_pth_issue(issue): + finding = \ + {"Severity": { + "Product": 1, + "Normalized": 100 + }, "Resources": [{ + "Type": "AwsEc2Instance", + "Id": issue['aws_instance_id'] + }], "RecordState": "ACTIVE", + "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']), "Remediation": { + "Recommendation": { + "Text": "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']) + } + }} + + return finding + + @staticmethod + def _handle_rdp_issue(issue): + finding = \ + {"Severity": { + "Product": 1, + "Normalized": 100 + }, "Resources": [{ + "Type": "AwsEc2Instance", + "Id": issue['aws_instance_id'] + }], "RecordState": "ACTIVE", + "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']), "Remediation": { + "Recommendation": { + "Text": "The machine machine ({ip_address}) is vulnerable to a RDP attack. The Monkey authenticated over the RDP protocol with user {username} and its password.".format( + machine=issue['machine'], ip_address=issue['ip_address'], username=issue['username']) + } + }} + + return finding + + @staticmethod + def _handle_shared_passwords_domain_issue(issue): + finding = \ + {"Severity": { + "Product": 1, + "Normalized": 100 + }, "Resources": [{ + "Type": "AwsEc2Instance", + "Id": issue['aws_instance_id'] + }], "RecordState": "ACTIVE", "Title": "Multiple users have the same password.", + "Description": "Some domain users are sharing passwords, this should be fixed by changing passwords.", + "Remediation": { + "Recommendation": { + "Text": "These users are sharing access password: {shared_with}.".format( + shared_with=issue['shared_with']) + } + }} + + return finding + + @staticmethod + def _handle_shared_admins_domain_issue(issue): + finding = \ + {"Severity": { + "Product": 1, + "Normalized": 100 + }, "Resources": [{ + "Type": "AwsEc2Instance", + "Id": issue['aws_instance_id'] + }], "RecordState": "ACTIVE", + "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.", + "Remediation": { + "Recommendation": { + "Text": "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']) + } + }} + + return finding + + @staticmethod + def _handle_strong_users_on_crit_issue(issue): + finding = \ + {"Severity": { + "Product": 1, + "Normalized": 100 + }, "Resources": [{ + "Type": "AwsEc2Instance", + "Id": issue['aws_instance_id'] + }], "RecordState": "ACTIVE", + "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.", + "Remediation": { + "Recommendation": { + "Text": "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']) + } + }} + + return finding + + @staticmethod + def _handle_struts2_issue(issue): + finding = \ + {"Severity": { + "Product": 10, + "Normalized": 100 + }, "Resources": [{ + "Type": "AwsEc2Instance", + "Id": issue['aws_instance_id'] + }], "RecordState": "ACTIVE", "Title": "Struts2 servers are vulnerable to remote code execution.", + "Description": "Upgrade Struts2 to version 2.3.32 or 2.5.10.1 or any later versions.", "Remediation": { + "Recommendation": { + "Text": "Struts2 server at {machine} ({ip_address}) is vulnerable to remote code execution attack." + " The attack was made possible because the server is using an old version of Jakarta based file upload Multipart parser.".format( + machine=issue['machine'], ip_address=issue['ip_address']) + } + }} + + return finding + + @staticmethod + def _handle_weblogic_issue(issue): + finding = \ + {"Severity": { + "Product": 10, + "Normalized": 100 + }, "Resources": [{ + "Type": "AwsEc2Instance", + "Id": issue['aws_instance_id'] + }], "RecordState": "ACTIVE", "Title": "Oracle WebLogic servers are vulnerable to remote code execution.", + "Description": "Install Oracle critical patch updates. Or update to the latest version. " \ + "Vulnerable versions are 10.3.6.0.0, 12.1.3.0.0, 12.2.1.1.0 and 12.2.1.2.0.", + "Remediation": { + "Recommendation": { + "Text": "Oracle WebLogic server at {machine} ({ip_address}) is vulnerable to remote code execution attack." + " The attack was made possible due to incorrect permission assignment in Oracle Fusion Middleware (subcomponent: WLS Security).".format( + machine=issue['machine'], ip_address=issue['ip_address']) + } + }} + + return finding + + @staticmethod + def _handle_hadoop_issue(issue): + finding = \ + {"Severity": { + "Product": 10, + "Normalized": 100 + }, "Resources": [{ + "Type": "AwsEc2Instance", + "Id": issue['aws_instance_id'] + }], "RecordState": "ACTIVE", "Title": "Hadoop/Yarn servers are vulnerable to remote code execution.", + "Description": "Run Hadoop in secure mode, add Kerberos authentication.", "Remediation": { + "Recommendation": { + "Text": "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." + } + }} + + return finding diff --git a/monkey/monkey_island/cc/services/config.py b/monkey/monkey_island/cc/services/config.py index 2058a61dd..9f61195f5 100644 --- a/monkey/monkey_island/cc/services/config.py +++ b/monkey/monkey_island/cc/services/config.py @@ -907,6 +907,7 @@ ENCRYPTED_CONFIG_ARRAYS = \ ENCRYPTED_CONFIG_STRINGS = \ [ ['cnc', 'aws_config', 'aws_access_key_id'], + ['cnc', 'aws_config', 'aws_account_id'], ['cnc', 'aws_config', 'aws_secret_access_key'] ] From 0a6b3a12fabd7167cd87563d2315552fb17b620b Mon Sep 17 00:00:00 2001 From: "maor.rayzin" Date: Tue, 27 Nov 2018 17:32:46 +0200 Subject: [PATCH 25/46] * Separated the configuration functions to support both island's and monkey's needs * Removed space char from the default value of the aws keys * Changed the submit function in the JS to point to the right endpoint --- monkey/monkey_island/cc/services/config.py | 14 +++++++++----- .../cc/ui/src/components/pages/ConfigurePage.js | 2 +- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/monkey/monkey_island/cc/services/config.py b/monkey/monkey_island/cc/services/config.py index 9f61195f5..8434a41dd 100644 --- a/monkey/monkey_island/cc/services/config.py +++ b/monkey/monkey_island/cc/services/config.py @@ -649,19 +649,19 @@ SCHEMA = { 'title': 'AWS account ID', 'type': 'string', 'description': 'Your AWS account ID that is subscribed to security hub feeds', - 'default': " " + 'default': "" }, 'aws_access_key_id': { 'title': 'AWS access key ID', 'type': 'string', 'description': 'Your AWS public access key ID, can be found in the IAM user interface in the AWS console.', - 'default': " " + 'default': "" }, 'aws_secret_access_key': { 'title': 'AWS secret access key', 'type': 'string', 'description': 'Your AWS secret access key id, you can get this after creating a public access key in the console.', - 'default': " " + 'default': "" } } } @@ -1107,11 +1107,15 @@ class ConfigService: ConfigService._encrypt_or_decrypt_config(config, False) @staticmethod - def decrypt_flat_config(flat_config): + def decrypt_flat_config(flat_config, is_island=False): """ Same as decrypt_config but for a flat configuration """ - keys = [config_arr_as_array[2] for config_arr_as_array in (ENCRYPTED_CONFIG_ARRAYS + ENCRYPTED_CONFIG_STRINGS)] + if is_island: + keys = [config_arr_as_array[2] for config_arr_as_array in + (ENCRYPTED_CONFIG_ARRAYS + ENCRYPTED_CONFIG_STRINGS)] + else: + keys = [config_arr_as_array[2] for config_arr_as_array in ENCRYPTED_CONFIG_ARRAYS] for key in keys: if isinstance(flat_config[key], collections.Sequence) and not isinstance(flat_config[key], string_types): # Check if we are decrypting ssh key pair diff --git a/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js b/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js index 6cc7e009a..ed8258197 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js @@ -44,7 +44,7 @@ class ConfigurePageComponent extends AuthComponent { onSubmit = ({formData}) => { this.currentFormData = formData; this.updateConfigSection(); - this.authFetch('/api/configuration', + this.authFetch('/api/configuration/island', { method: 'POST', headers: {'Content-Type': 'application/json'}, From af97fb6ffc53e912f0b4798a50a835c5d42c3fac Mon Sep 17 00:00:00 2001 From: "maor.rayzin" Date: Tue, 27 Nov 2018 17:45:31 +0200 Subject: [PATCH 26/46] * Added a check to no issues list * Changed the productARN to the monkey's ARN --- monkey/monkey_island/cc/resources/aws_exporter.py | 3 +++ monkey/monkey_island/cc/server_config.json | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/monkey/monkey_island/cc/resources/aws_exporter.py b/monkey/monkey_island/cc/resources/aws_exporter.py index 0d9b0a157..e7221a668 100644 --- a/monkey/monkey_island/cc/resources/aws_exporter.py +++ b/monkey/monkey_island/cc/resources/aws_exporter.py @@ -21,6 +21,9 @@ class AWSExporter(Exporter): 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 for machine in issues_list: for issue in issues_list[machine]: findings_list.append(AWSExporter._prepare_finding(issue)) diff --git a/monkey/monkey_island/cc/server_config.json b/monkey/monkey_island/cc/server_config.json index 4d8644cbb..82211562f 100644 --- a/monkey/monkey_island/cc/server_config.json +++ b/monkey/monkey_island/cc/server_config.json @@ -1,6 +1,6 @@ { "server_config": "standard", "aws": { - "sec_hub_product_arn": "arn:aws:securityhub:us-west-2:324264561773:product/aws/guardduty" + "sec_hub_product_arn": "arn:aws:securityhub:eu-west-2:324264561773:product/guardicore/aws-infection-monkey" } } \ No newline at end of file From e8c604d7c5b06f6e9be6317c7740aa410ec78491 Mon Sep 17 00:00:00 2001 From: "maor.rayzin" Date: Tue, 27 Nov 2018 17:48:40 +0200 Subject: [PATCH 27/46] * Changed the exporter to work in aws and not standard (was used for debugging) --- monkey/monkey_island/cc/services/report.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/monkey_island/cc/services/report.py b/monkey/monkey_island/cc/services/report.py index 961bb1195..2a60ffa12 100644 --- a/monkey/monkey_island/cc/services/report.py +++ b/monkey/monkey_island/cc/services/report.py @@ -754,7 +754,7 @@ class ReportService: def get_active_exporters(): # This function should be in another module in charge of building a list of active exporters exporters_list = [] - if str(load_env_from_file()) == 'standard': + if str(load_env_from_file()) == AWS: exporters_list.append(AWSExporter) return exporters_list From 2f1240cc0e342ccf6ef438ed7e361722d8ed6943 Mon Sep 17 00:00:00 2001 From: "maor.rayzin" Date: Tue, 27 Nov 2018 20:21:39 +0200 Subject: [PATCH 28/46] * Added the boto3 pckg to the right req'.txt file * Added a safe dict key access for aws_instance_id in report.py * Added a skip in the aws_export if there is no instance_id in the issue. --- monkey/monkey_island/cc/resources/aws_exporter.py | 3 ++- monkey/monkey_island/cc/services/report.py | 2 +- .../deb-package/monkey_island_pip_requirements.txt | 1 + 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/monkey/monkey_island/cc/resources/aws_exporter.py b/monkey/monkey_island/cc/resources/aws_exporter.py index e7221a668..44dd94859 100644 --- a/monkey/monkey_island/cc/resources/aws_exporter.py +++ b/monkey/monkey_island/cc/resources/aws_exporter.py @@ -26,7 +26,8 @@ class AWSExporter(Exporter): return True for machine in issues_list: for issue in issues_list[machine]: - findings_list.append(AWSExporter._prepare_finding(issue)) + if not issue.get('aws_instance_id', None): + findings_list.append(AWSExporter._prepare_finding(issue)) if not AWSExporter._send_findings(findings_list, AWSExporter._get_aws_keys()): logger.error('Exporting findings to aws failed') diff --git a/monkey/monkey_island/cc/services/report.py b/monkey/monkey_island/cc/services/report.py index 2a60ffa12..b89266cad 100644 --- a/monkey/monkey_island/cc/services/report.py +++ b/monkey/monkey_island/cc/services/report.py @@ -553,7 +553,7 @@ class ReportService: @staticmethod def get_machine_aws_instance_id(hostname): - return str(list(mongo.db.monkey.find({'hostname': hostname}, {'aws_instance_id': 1}))[0]['aws_instance_id']) + return str(list(mongo.db.monkey.find({'hostname': hostname}, {'aws_instance_id': 1}))[0].get('aws_instance_id', None)) @staticmethod def get_issues(): diff --git a/monkey/monkey_island/deb-package/monkey_island_pip_requirements.txt b/monkey/monkey_island/deb-package/monkey_island_pip_requirements.txt index 446414ecf..7046bf231 100644 --- a/monkey/monkey_island/deb-package/monkey_island_pip_requirements.txt +++ b/monkey/monkey_island/deb-package/monkey_island_pip_requirements.txt @@ -14,4 +14,5 @@ netifaces ipaddress enum34 PyCrypto +boto3 virtualenv \ No newline at end of file From 83ea8af9e023c8320d3273ee2f478951c001ca19 Mon Sep 17 00:00:00 2001 From: "maor.rayzin" Date: Tue, 27 Nov 2018 22:28:06 +0200 Subject: [PATCH 29/46] * Added error handling in case the aws cli wasn't properly installed. --- monkey/monkey_island/cc/resources/aws_exporter.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/monkey/monkey_island/cc/resources/aws_exporter.py b/monkey/monkey_island/cc/resources/aws_exporter.py index 44dd94859..4027170bd 100644 --- a/monkey/monkey_island/cc/resources/aws_exporter.py +++ b/monkey/monkey_island/cc/resources/aws_exporter.py @@ -2,6 +2,7 @@ import logging import uuid from datetime import datetime import boto3 +from botocore.exceptions import UnknownServiceError from cc.resources.exporter import Exporter from cc.services.config import ConfigService @@ -93,17 +94,20 @@ class AWSExporter(Exporter): @staticmethod def _send_findings(findings_list, creds_dict): - - securityhub = boto3.client('securityhub', - aws_access_key_id=creds_dict.get('aws_access_key_id', ''), - aws_secret_access_key=creds_dict.get('aws_secret_access_key', '')) try: + securityhub = boto3.client('securityhub', + aws_access_key_id=creds_dict.get('aws_access_key_id', ''), + aws_secret_access_key=creds_dict.get('aws_secret_access_key', '')) + import_response = securityhub.batch_import_findings(Findings=findings_list) print import_response if import_response['ResponseMetadata']['HTTPStatusCode'] == 200: return True else: return False + except UnknownServiceError as e: + logger.warning('AWS exporter called but AWS-CLI not installed') + return False except Exception as e: logger.error('AWS security hub findings failed to send.') return False From 7d94185a102a40af83e77df946c7a942cd75cc7a Mon Sep 17 00:00:00 2001 From: "maor.rayzin" Date: Wed, 28 Nov 2018 12:53:58 +0200 Subject: [PATCH 30/46] * fixed a wrong IF statement that prevented issues from appending --- monkey/monkey_island/cc/resources/aws_exporter.py | 2 +- monkey/monkey_island/cc/services/report.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey/monkey_island/cc/resources/aws_exporter.py b/monkey/monkey_island/cc/resources/aws_exporter.py index 4027170bd..9ebb28331 100644 --- a/monkey/monkey_island/cc/resources/aws_exporter.py +++ b/monkey/monkey_island/cc/resources/aws_exporter.py @@ -27,7 +27,7 @@ class AWSExporter(Exporter): return True for machine in issues_list: for issue in issues_list[machine]: - if not issue.get('aws_instance_id', None): + if issue.get('aws_instance_id', None): findings_list.append(AWSExporter._prepare_finding(issue)) if not AWSExporter._send_findings(findings_list, AWSExporter._get_aws_keys()): diff --git a/monkey/monkey_island/cc/services/report.py b/monkey/monkey_island/cc/services/report.py index b89266cad..09e12edcd 100644 --- a/monkey/monkey_island/cc/services/report.py +++ b/monkey/monkey_island/cc/services/report.py @@ -754,7 +754,7 @@ class ReportService: def get_active_exporters(): # This function should be in another module in charge of building a list of active exporters exporters_list = [] - if str(load_env_from_file()) == AWS: + if str(load_env_from_file()) == 'standard': exporters_list.append(AWSExporter) return exporters_list From bdecc7ade6ce4c0d7e6e3080d8053876c572609d Mon Sep 17 00:00:00 2001 From: "maor.rayzin" Date: Wed, 28 Nov 2018 13:27:35 +0200 Subject: [PATCH 31/46] * added dynamic region lookup * building the product ARN dynamically * Resource type is now Other in case we dont have instance_id --- .../cc/resources/aws_exporter.py | 253 ++++++++++++------ monkey/monkey_island/cc/server_config.json | 2 +- 2 files changed, 176 insertions(+), 79 deletions(-) diff --git a/monkey/monkey_island/cc/resources/aws_exporter.py b/monkey/monkey_island/cc/resources/aws_exporter.py index 9ebb28331..d98cececd 100644 --- a/monkey/monkey_island/cc/resources/aws_exporter.py +++ b/monkey/monkey_island/cc/resources/aws_exporter.py @@ -7,6 +7,7 @@ from botocore.exceptions import UnknownServiceError from cc.resources.exporter import Exporter from cc.services.config import ConfigService from cc.environment.environment import load_server_configuration_from_file +from common.cloud.aws import AWS logger = logging.getLogger(__name__) @@ -75,7 +76,9 @@ class AWSExporter(Exporter): # azure and conficker are not relevant issues for an AWS env } - product_arn = load_server_configuration_from_file()['aws'].get('sec_hub_product_arn', '') + aws = AWS() + configured_product_arn = load_server_configuration_from_file()['aws'].get('sec_hub_product_arn', '') + product_arn = 'arn:aws:securityhub:{region}:{arn}'.format(region=aws.get_region(), arn=configured_product_arn) account_id = AWSExporter._get_aws_keys().get('aws_account_id', '') finding = { @@ -118,10 +121,7 @@ class AWSExporter(Exporter): {"Severity": { "Product": 5, "Normalized": 100 - }, "Resources": [{ - "Type": "AwsEc2Instance", - "Id": issue['aws_instance_id'] - }], "RecordState": "ACTIVE", + }, "RecordState": "ACTIVE", "Title": "Weak segmentation - Machines were able to communicate over unused ports.", "Description": "Use micro-segmentation policies to disable communication other than the required.", "Remediation": { @@ -131,6 +131,14 @@ class AWSExporter(Exporter): } }} + if 'aws_instance_id' in issue: + finding["Resources"] = [{ + "Type": "AwsEc2Instance", + "Id": issue['aws_instance_id'] + }] + else: + finding["Resources"] = [{'Type': 'Other'}] + return finding @staticmethod @@ -139,18 +147,23 @@ class AWSExporter(Exporter): {"Severity": { "Product": 10, "Normalized": 100 - }, "Resources": [{ - "Type": "AwsEc2Instance", - "Id": issue['aws_instance_id'] - }], "RecordState": "ACTIVE", "Title": "Samba servers are vulnerable to 'SambaCry'", + }, "RecordState": "ACTIVE", "Title": "Samba servers are vulnerable to 'SambaCry'", "Description": "Change {0} password to a complex one-use password that is not shared with other computers on the network. Update your Samba server to 4.4.14 and up, 4.5.10 and up, or 4.6.4 and up." \ - .format(issue['username']), "Remediation": { + .format(issue['username']), "Remediation": { "Recommendation": { "Text": "The machine {0} ({1}) is vulnerable to a SambaCry attack. The Monkey authenticated over the SMB protocol with user {2} and its password, and used the SambaCry vulnerability.".format( issue['machine'], issue['ip_address'], issue['username']) } }} + if 'aws_instance_id' in issue: + finding["Resources"] = [{ + "Type": "AwsEc2Instance", + "Id": issue['aws_instance_id'] + }] + else: + finding["Resources"] = [{'Type': 'Other'}] + return finding @staticmethod @@ -159,10 +172,7 @@ class AWSExporter(Exporter): {"Severity": { "Product": 5, "Normalized": 100 - }, "Resources": [{ - "Type": "AwsEc2Instance", - "Id": issue['aws_instance_id'] - }], "RecordState": "ACTIVE", + }, "RecordState": "ACTIVE", "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']), "Remediation": { @@ -172,6 +182,14 @@ class AWSExporter(Exporter): } }} + if 'aws_instance_id' in issue: + finding["Resources"] = [{ + "Type": "AwsEc2Instance", + "Id": issue['aws_instance_id'] + }] + else: + finding["Resources"] = [{'Type': 'Other'}] + return finding @staticmethod @@ -180,10 +198,7 @@ class AWSExporter(Exporter): {"Severity": { "Product": 1, "Normalized": 100 - }, "Resources": [{ - "Type": "AwsEc2Instance", - "Id": issue['aws_instance_id'] - }], "RecordState": "ACTIVE", + }, "RecordState": "ACTIVE", "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']), "Remediation": { @@ -193,6 +208,14 @@ class AWSExporter(Exporter): } }} + if 'aws_instance_id' in issue: + finding["Resources"] = [{ + "Type": "AwsEc2Instance", + "Id": issue['aws_instance_id'] + }] + else: + finding["Resources"] = [{'Type': 'Other'}] + return finding @staticmethod @@ -201,10 +224,7 @@ class AWSExporter(Exporter): {"Severity": { "Product": 1, "Normalized": 100 - }, "Resources": [{ - "Type": "AwsEc2Instance", - "Id": issue['aws_instance_id'] - }], "RecordState": "ACTIVE", + }, "RecordState": "ACTIVE", "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']), "Remediation": { @@ -214,6 +234,13 @@ class AWSExporter(Exporter): } }} + if 'aws_instance_id' in issue: + finding["Resources"] = [{ + "Type": "AwsEc2Instance", + "Id": issue['aws_instance_id'] + }] + else: + finding["Resources"] = [{'Type': 'Other'}] return finding @staticmethod @@ -222,10 +249,7 @@ class AWSExporter(Exporter): {"Severity": { "Product": 10, "Normalized": 100 - }, "Resources": [{ - "Type": "AwsEc2Instance", - "Id": issue['aws_instance_id'] - }], "RecordState": "ACTIVE", "Title": "Elasticsearch servers are vulnerable to CVE-2015-1427", + }, "RecordState": "ACTIVE", "Title": "Elasticsearch servers are vulnerable to CVE-2015-1427", "Description": "Update your Elastic Search server to version 1.4.3 and up.", "Remediation": { "Recommendation": { "Text": "The machine {0}({1}) is vulnerable to an Elastic Groovy attack. The attack was made possible because the Elastic Search server was not patched against CVE-2015-1427.".format( @@ -233,6 +257,14 @@ class AWSExporter(Exporter): } }} + if 'aws_instance_id' in issue: + finding["Resources"] = [{ + "Type": "AwsEc2Instance", + "Id": issue['aws_instance_id'] + }] + else: + finding["Resources"] = [{'Type': 'Other'}] + return finding @staticmethod @@ -241,10 +273,7 @@ class AWSExporter(Exporter): {"Severity": { "Product": 1, "Normalized": 100 - }, "Resources": [{ - "Type": "AwsEc2Instance", - "Id": issue['aws_instance_id'] - }], "RecordState": "ACTIVE", + }, "RecordState": "ACTIVE", "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.", "Remediation": { @@ -257,6 +286,14 @@ class AWSExporter(Exporter): } }} + if 'aws_instance_id' in issue: + finding["Resources"] = [{ + "Type": "AwsEc2Instance", + "Id": issue['aws_instance_id'] + }] + else: + finding["Resources"] = [{'Type': 'Other'}] + return finding @staticmethod @@ -265,10 +302,7 @@ class AWSExporter(Exporter): {"Severity": { "Product": 1, "Normalized": 100 - }, "Resources": [{ - "Type": "AwsEc2Instance", - "Id": issue['aws_instance_id'] - }], "RecordState": "ACTIVE", "Title": "Multiple users have the same password", + }, "RecordState": "ACTIVE", "Title": "Multiple users have the same password", "Description": "Some users are sharing passwords, this should be fixed by changing passwords.", "Remediation": { "Recommendation": { @@ -276,6 +310,14 @@ class AWSExporter(Exporter): } }} + if 'aws_instance_id' in issue: + finding["Resources"] = [{ + "Type": "AwsEc2Instance", + "Id": issue['aws_instance_id'] + }] + else: + finding["Resources"] = [{'Type': 'Other'}] + return finding @staticmethod @@ -284,10 +326,7 @@ class AWSExporter(Exporter): {"Severity": { "Product": 10, "Normalized": 100 - }, "Resources": [{ - "Type": "AwsEc2Instance", - "Id": issue['aws_instance_id'] - }], "RecordState": "ACTIVE", "Title": "Machines are vulnerable to 'Shellshock'", + }, "RecordState": "ACTIVE", "Title": "Machines are vulnerable to 'Shellshock'", "Description": "Update your Bash to a ShellShock-patched version.", "Remediation": { "Recommendation": { "Text": "The machine {0} ({1}) is vulnerable to a ShellShock attack. " @@ -296,6 +335,14 @@ class AWSExporter(Exporter): } }} + if 'aws_instance_id' in issue: + finding["Resources"] = [{ + "Type": "AwsEc2Instance", + "Id": issue['aws_instance_id'] + }] + else: + finding["Resources"] = [{'Type': 'Other'}] + return finding @staticmethod @@ -304,10 +351,7 @@ class AWSExporter(Exporter): {"Severity": { "Product": 1, "Normalized": 100 - }, "Resources": [{ - "Type": "AwsEc2Instance", - "Id": issue['aws_instance_id'] - }], "RecordState": "ACTIVE", + }, "RecordState": "ACTIVE", "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']), "Remediation": { @@ -317,6 +361,14 @@ class AWSExporter(Exporter): } }} + if 'aws_instance_id' in issue: + finding["Resources"] = [{ + "Type": "AwsEc2Instance", + "Id": issue['aws_instance_id'] + }] + else: + finding["Resources"] = [{'Type': 'Other'}] + return finding @staticmethod @@ -325,10 +377,7 @@ class AWSExporter(Exporter): {"Severity": { "Product": 1, "Normalized": 100 - }, "Resources": [{ - "Type": "AwsEc2Instance", - "Id": issue['aws_instance_id'] - }], "RecordState": "ACTIVE", + }, "RecordState": "ACTIVE", "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.", "Remediation": { @@ -338,6 +387,14 @@ class AWSExporter(Exporter): } }} + if 'aws_instance_id' in issue: + finding["Resources"] = [{ + "Type": "AwsEc2Instance", + "Id": issue['aws_instance_id'] + }] + else: + finding["Resources"] = [{'Type': 'Other'}] + return finding @staticmethod @@ -346,10 +403,7 @@ class AWSExporter(Exporter): {"Severity": { "Product": 1, "Normalized": 100 - }, "Resources": [{ - "Type": "AwsEc2Instance", - "Id": issue['aws_instance_id'] - }], "RecordState": "ACTIVE", + }, "RecordState": "ACTIVE", "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']), "Remediation": { @@ -359,6 +413,14 @@ class AWSExporter(Exporter): } }} + if 'aws_instance_id' in issue: + finding["Resources"] = [{ + "Type": "AwsEc2Instance", + "Id": issue['aws_instance_id'] + }] + else: + finding["Resources"] = [{'Type': 'Other'}] + return finding @staticmethod @@ -367,10 +429,7 @@ class AWSExporter(Exporter): {"Severity": { "Product": 1, "Normalized": 100 - }, "Resources": [{ - "Type": "AwsEc2Instance", - "Id": issue['aws_instance_id'] - }], "RecordState": "ACTIVE", + }, "RecordState": "ACTIVE", "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']), "Remediation": { @@ -380,6 +439,14 @@ class AWSExporter(Exporter): } }} + if 'aws_instance_id' in issue: + finding["Resources"] = [{ + "Type": "AwsEc2Instance", + "Id": issue['aws_instance_id'] + }] + else: + finding["Resources"] = [{'Type': 'Other'}] + return finding @staticmethod @@ -388,10 +455,7 @@ class AWSExporter(Exporter): {"Severity": { "Product": 1, "Normalized": 100 - }, "Resources": [{ - "Type": "AwsEc2Instance", - "Id": issue['aws_instance_id'] - }], "RecordState": "ACTIVE", "Title": "Multiple users have the same password.", + }, "RecordState": "ACTIVE", "Title": "Multiple users have the same password.", "Description": "Some domain users are sharing passwords, this should be fixed by changing passwords.", "Remediation": { "Recommendation": { @@ -400,6 +464,14 @@ class AWSExporter(Exporter): } }} + if 'aws_instance_id' in issue: + finding["Resources"] = [{ + "Type": "AwsEc2Instance", + "Id": issue['aws_instance_id'] + }] + else: + finding["Resources"] = [{'Type': 'Other'}] + return finding @staticmethod @@ -408,10 +480,7 @@ class AWSExporter(Exporter): {"Severity": { "Product": 1, "Normalized": 100 - }, "Resources": [{ - "Type": "AwsEc2Instance", - "Id": issue['aws_instance_id'] - }], "RecordState": "ACTIVE", + }, "RecordState": "ACTIVE", "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.", "Remediation": { @@ -421,6 +490,14 @@ class AWSExporter(Exporter): } }} + if 'aws_instance_id' in issue: + finding["Resources"] = [{ + "Type": "AwsEc2Instance", + "Id": issue['aws_instance_id'] + }] + else: + finding["Resources"] = [{'Type': 'Other'}] + return finding @staticmethod @@ -429,10 +506,7 @@ class AWSExporter(Exporter): {"Severity": { "Product": 1, "Normalized": 100 - }, "Resources": [{ - "Type": "AwsEc2Instance", - "Id": issue['aws_instance_id'] - }], "RecordState": "ACTIVE", + }, "RecordState": "ACTIVE", "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.", "Remediation": { @@ -442,6 +516,14 @@ class AWSExporter(Exporter): } }} + if 'aws_instance_id' in issue: + finding["Resources"] = [{ + "Type": "AwsEc2Instance", + "Id": issue['aws_instance_id'] + }] + else: + finding["Resources"] = [{'Type': 'Other'}] + return finding @staticmethod @@ -450,10 +532,7 @@ class AWSExporter(Exporter): {"Severity": { "Product": 10, "Normalized": 100 - }, "Resources": [{ - "Type": "AwsEc2Instance", - "Id": issue['aws_instance_id'] - }], "RecordState": "ACTIVE", "Title": "Struts2 servers are vulnerable to remote code execution.", + }, "RecordState": "ACTIVE", "Title": "Struts2 servers are vulnerable to remote code execution.", "Description": "Upgrade Struts2 to version 2.3.32 or 2.5.10.1 or any later versions.", "Remediation": { "Recommendation": { "Text": "Struts2 server at {machine} ({ip_address}) is vulnerable to remote code execution attack." @@ -462,6 +541,14 @@ class AWSExporter(Exporter): } }} + if 'aws_instance_id' in issue: + finding["Resources"] = [{ + "Type": "AwsEc2Instance", + "Id": issue['aws_instance_id'] + }] + else: + finding["Resources"] = [{'Type': 'Other'}] + return finding @staticmethod @@ -470,10 +557,7 @@ class AWSExporter(Exporter): {"Severity": { "Product": 10, "Normalized": 100 - }, "Resources": [{ - "Type": "AwsEc2Instance", - "Id": issue['aws_instance_id'] - }], "RecordState": "ACTIVE", "Title": "Oracle WebLogic servers are vulnerable to remote code execution.", + }, "RecordState": "ACTIVE", "Title": "Oracle WebLogic servers are vulnerable to remote code execution.", "Description": "Install Oracle critical patch updates. Or update to the latest version. " \ "Vulnerable versions are 10.3.6.0.0, 12.1.3.0.0, 12.2.1.1.0 and 12.2.1.2.0.", "Remediation": { @@ -484,6 +568,14 @@ class AWSExporter(Exporter): } }} + if 'aws_instance_id' in issue: + finding["Resources"] = [{ + "Type": "AwsEc2Instance", + "Id": issue['aws_instance_id'] + }] + else: + finding["Resources"] = [{'Type': 'Other'}] + return finding @staticmethod @@ -492,10 +584,7 @@ class AWSExporter(Exporter): {"Severity": { "Product": 10, "Normalized": 100 - }, "Resources": [{ - "Type": "AwsEc2Instance", - "Id": issue['aws_instance_id'] - }], "RecordState": "ACTIVE", "Title": "Hadoop/Yarn servers are vulnerable to remote code execution.", + }, "RecordState": "ACTIVE", "Title": "Hadoop/Yarn servers are vulnerable to remote code execution.", "Description": "Run Hadoop in secure mode, add Kerberos authentication.", "Remediation": { "Recommendation": { "Text": "The Hadoop server at {machine} ({ip_address}) is vulnerable to remote code execution attack." @@ -503,4 +592,12 @@ class AWSExporter(Exporter): } }} + if 'aws_instance_id' in issue: + finding["Resources"] = [{ + "Type": "AwsEc2Instance", + "Id": issue['aws_instance_id'] + }] + else: + finding["Resources"] = [{'Type': 'Other'}] + return finding diff --git a/monkey/monkey_island/cc/server_config.json b/monkey/monkey_island/cc/server_config.json index 82211562f..3ca292587 100644 --- a/monkey/monkey_island/cc/server_config.json +++ b/monkey/monkey_island/cc/server_config.json @@ -1,6 +1,6 @@ { "server_config": "standard", "aws": { - "sec_hub_product_arn": "arn:aws:securityhub:eu-west-2:324264561773:product/guardicore/aws-infection-monkey" + "sec_hub_product_arn": "324264561773:product/guardicore/aws-infection-monkey" } } \ No newline at end of file From 8397af4c6b6ee2db5521aa9fde7a09cf5e16b2e1 Mon Sep 17 00:00:00 2001 From: "maor.rayzin" Date: Wed, 28 Nov 2018 14:56:46 +0200 Subject: [PATCH 32/46] * Added region to finding sending configuration for boto3 --- .../monkey_island/cc/resources/aws_exporter.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/monkey/monkey_island/cc/resources/aws_exporter.py b/monkey/monkey_island/cc/resources/aws_exporter.py index d98cececd..ab9c74185 100644 --- a/monkey/monkey_island/cc/resources/aws_exporter.py +++ b/monkey/monkey_island/cc/resources/aws_exporter.py @@ -20,7 +20,7 @@ class AWSExporter(Exporter): @staticmethod def handle_report(report_json): - + aws = AWS() findings_list = [] issues_list = report_json['recommendations']['issues'] if not issues_list: @@ -29,9 +29,9 @@ class AWSExporter(Exporter): for machine in issues_list: for issue in issues_list[machine]: if issue.get('aws_instance_id', None): - findings_list.append(AWSExporter._prepare_finding(issue)) + findings_list.append(AWSExporter._prepare_finding(issue, aws.get_region())) - if not AWSExporter._send_findings(findings_list, AWSExporter._get_aws_keys()): + if not AWSExporter._send_findings(findings_list, AWSExporter._get_aws_keys(), aws.get_region()): logger.error('Exporting findings to aws failed') return False @@ -52,7 +52,7 @@ class AWSExporter(Exporter): return z @staticmethod - def _prepare_finding(issue): + def _prepare_finding(issue, region): findings_dict = { 'island_cross_segment': AWSExporter._handle_island_cross_segment_issue, 'ssh': AWSExporter._handle_ssh_issue, @@ -76,9 +76,8 @@ class AWSExporter(Exporter): # azure and conficker are not relevant issues for an AWS env } - aws = AWS() configured_product_arn = load_server_configuration_from_file()['aws'].get('sec_hub_product_arn', '') - product_arn = 'arn:aws:securityhub:{region}:{arn}'.format(region=aws.get_region(), arn=configured_product_arn) + product_arn = 'arn:aws:securityhub:{region}:{arn}'.format(region=region, arn=configured_product_arn) account_id = AWSExporter._get_aws_keys().get('aws_account_id', '') finding = { @@ -96,11 +95,12 @@ class AWSExporter(Exporter): return AWSExporter.merge_two_dicts(finding, findings_dict[issue['type']](issue)) @staticmethod - def _send_findings(findings_list, creds_dict): + def _send_findings(findings_list, creds_dict, region): try: securityhub = boto3.client('securityhub', aws_access_key_id=creds_dict.get('aws_access_key_id', ''), - aws_secret_access_key=creds_dict.get('aws_secret_access_key', '')) + aws_secret_access_key=creds_dict.get('aws_secret_access_key', ''), + region_name=region) import_response = securityhub.batch_import_findings(Findings=findings_list) print import_response From 0fe7a9c6e1027aa4727bc23e1aae7f0c9d01f568 Mon Sep 17 00:00:00 2001 From: "maor.rayzin" Date: Wed, 28 Nov 2018 15:02:17 +0200 Subject: [PATCH 33/46] * Match it back to aws env --- monkey/monkey_island/cc/services/report.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/monkey_island/cc/services/report.py b/monkey/monkey_island/cc/services/report.py index 09e12edcd..b89266cad 100644 --- a/monkey/monkey_island/cc/services/report.py +++ b/monkey/monkey_island/cc/services/report.py @@ -754,7 +754,7 @@ class ReportService: def get_active_exporters(): # This function should be in another module in charge of building a list of active exporters exporters_list = [] - if str(load_env_from_file()) == 'standard': + if str(load_env_from_file()) == AWS: exporters_list.append(AWSExporter) return exporters_list From 9d36cf399008e2319db7945630177bb3ab247d6d Mon Sep 17 00:00:00 2001 From: "maor.rayzin" Date: Wed, 28 Nov 2018 15:30:46 +0200 Subject: [PATCH 34/46] * add the right key in telemetry * added error handling in report.py --- monkey/monkey_island/cc/resources/telemetry.py | 3 ++- monkey/monkey_island/cc/services/report.py | 6 +++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/monkey/monkey_island/cc/resources/telemetry.py b/monkey/monkey_island/cc/resources/telemetry.py index ab911a119..c5d9ef8a6 100644 --- a/monkey/monkey_island/cc/resources/telemetry.py +++ b/monkey/monkey_island/cc/resources/telemetry.py @@ -192,7 +192,8 @@ class Telemetry(flask_restful.Resource): wmi_handler = WMIHandler(monkey_id, telemetry_json['data']['wmi'], users_secrets) wmi_handler.process_and_handle_wmi_info() if 'aws' in telemetry_json['data']: - mongo.db.monkey.update_one({'_id': monkey_id}, {'aws_instance_id': telemetry_json['data']['instance-id']}) + mongo.db.monkey.update_one({'_id': monkey_id}, + {'aws_instance_id': telemetry_json['data']['aws']['instance-id']}) @staticmethod def add_ip_to_ssh_keys(ip, ssh_info): diff --git a/monkey/monkey_island/cc/services/report.py b/monkey/monkey_island/cc/services/report.py index b89266cad..3120194a3 100644 --- a/monkey/monkey_island/cc/services/report.py +++ b/monkey/monkey_island/cc/services/report.py @@ -553,7 +553,11 @@ class ReportService: @staticmethod def get_machine_aws_instance_id(hostname): - return str(list(mongo.db.monkey.find({'hostname': hostname}, {'aws_instance_id': 1}))[0].get('aws_instance_id', None)) + aws_instance_id_list = list(mongo.db.monkey.find({'hostname': hostname}, {'aws_instance_id': 1})) + if aws_instance_id_list: + return str(aws_instance_id_list[0].get('aws_instance_id', None)) + else: + return None @staticmethod def get_issues(): From 25340e99986e9776a4e24b853e2d2986d73c514a Mon Sep 17 00:00:00 2001 From: "maor.rayzin" Date: Wed, 28 Nov 2018 17:05:10 +0200 Subject: [PATCH 35/46] * Deleted print statement * Added further inspection in telemtry --- monkey/monkey_island/cc/resources/aws_exporter.py | 1 - monkey/monkey_island/cc/resources/telemetry.py | 5 +++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/monkey/monkey_island/cc/resources/aws_exporter.py b/monkey/monkey_island/cc/resources/aws_exporter.py index ab9c74185..a3ee0309a 100644 --- a/monkey/monkey_island/cc/resources/aws_exporter.py +++ b/monkey/monkey_island/cc/resources/aws_exporter.py @@ -103,7 +103,6 @@ class AWSExporter(Exporter): region_name=region) import_response = securityhub.batch_import_findings(Findings=findings_list) - print import_response if import_response['ResponseMetadata']['HTTPStatusCode'] == 200: return True else: diff --git a/monkey/monkey_island/cc/resources/telemetry.py b/monkey/monkey_island/cc/resources/telemetry.py index c5d9ef8a6..581cbf3dc 100644 --- a/monkey/monkey_island/cc/resources/telemetry.py +++ b/monkey/monkey_island/cc/resources/telemetry.py @@ -192,8 +192,9 @@ class Telemetry(flask_restful.Resource): wmi_handler = WMIHandler(monkey_id, telemetry_json['data']['wmi'], users_secrets) wmi_handler.process_and_handle_wmi_info() if 'aws' in telemetry_json['data']: - mongo.db.monkey.update_one({'_id': monkey_id}, - {'aws_instance_id': telemetry_json['data']['aws']['instance-id']}) + if 'instance-id' in telemetry_json['data']['aws']: + mongo.db.monkey.update_one({'_id': monkey_id}, + {'aws_instance_id': telemetry_json['data']['aws']['instance-id']}) @staticmethod def add_ip_to_ssh_keys(ip, ssh_info): From 1c99636414769d2e405b3dbf56e504821a299bc7 Mon Sep 17 00:00:00 2001 From: "maor.rayzin" Date: Wed, 28 Nov 2018 17:40:32 +0200 Subject: [PATCH 36/46] * Changed the resource id to be instance arn and not only instance id --- .../cc/resources/aws_exporter.py | 81 ++++++++++--------- 1 file changed, 41 insertions(+), 40 deletions(-) diff --git a/monkey/monkey_island/cc/resources/aws_exporter.py b/monkey/monkey_island/cc/resources/aws_exporter.py index a3ee0309a..412b8390a 100644 --- a/monkey/monkey_island/cc/resources/aws_exporter.py +++ b/monkey/monkey_island/cc/resources/aws_exporter.py @@ -77,7 +77,8 @@ class AWSExporter(Exporter): } configured_product_arn = load_server_configuration_from_file()['aws'].get('sec_hub_product_arn', '') - product_arn = 'arn:aws:securityhub:{region}:{arn}'.format(region=region, arn=configured_product_arn) + product_arn = 'arn:aws:securityhub:{region}:{arn}'.format(region='us-west-2', arn=configured_product_arn) + instance_arn = 'arn:aws:ec2:' + region + ':instance:{instance_id}' account_id = AWSExporter._get_aws_keys().get('aws_account_id', '') finding = { @@ -92,7 +93,7 @@ class AWSExporter(Exporter): "CreatedAt": datetime.now().isoformat() + 'Z', "UpdatedAt": datetime.now().isoformat() + 'Z', } - return AWSExporter.merge_two_dicts(finding, findings_dict[issue['type']](issue)) + return AWSExporter.merge_two_dicts(finding, findings_dict[issue['type']](issue, instance_arn)) @staticmethod def _send_findings(findings_list, creds_dict, region): @@ -115,7 +116,7 @@ class AWSExporter(Exporter): return False @staticmethod - def _handle_tunnel_issue(issue): + def _handle_tunnel_issue(issue, instance_arn): finding = \ {"Severity": { "Product": 5, @@ -133,7 +134,7 @@ class AWSExporter(Exporter): if 'aws_instance_id' in issue: finding["Resources"] = [{ "Type": "AwsEc2Instance", - "Id": issue['aws_instance_id'] + "Id": instance_arn.format(instance_id=issue['aws_instance_id']) }] else: finding["Resources"] = [{'Type': 'Other'}] @@ -141,7 +142,7 @@ class AWSExporter(Exporter): return finding @staticmethod - def _handle_sambacry_issue(issue): + def _handle_sambacry_issue(issue, instance_arn): finding = \ {"Severity": { "Product": 10, @@ -158,7 +159,7 @@ class AWSExporter(Exporter): if 'aws_instance_id' in issue: finding["Resources"] = [{ "Type": "AwsEc2Instance", - "Id": issue['aws_instance_id'] + "Id": instance_arn.format(instance_id=issue['aws_instance_id']) }] else: finding["Resources"] = [{'Type': 'Other'}] @@ -166,7 +167,7 @@ class AWSExporter(Exporter): return finding @staticmethod - def _handle_smb_pth_issue(issue): + def _handle_smb_pth_issue(issue, instance_arn): finding = \ {"Severity": { "Product": 5, @@ -184,7 +185,7 @@ class AWSExporter(Exporter): if 'aws_instance_id' in issue: finding["Resources"] = [{ "Type": "AwsEc2Instance", - "Id": issue['aws_instance_id'] + "Id": instance_arn.format(instance_id=issue['aws_instance_id']) }] else: finding["Resources"] = [{'Type': 'Other'}] @@ -192,7 +193,7 @@ class AWSExporter(Exporter): return finding @staticmethod - def _handle_ssh_issue(issue): + def _handle_ssh_issue(issue, instance_arn): finding = \ {"Severity": { "Product": 1, @@ -210,7 +211,7 @@ class AWSExporter(Exporter): if 'aws_instance_id' in issue: finding["Resources"] = [{ "Type": "AwsEc2Instance", - "Id": issue['aws_instance_id'] + "Id": instance_arn.format(instance_id=issue['aws_instance_id']) }] else: finding["Resources"] = [{'Type': 'Other'}] @@ -218,7 +219,7 @@ class AWSExporter(Exporter): return finding @staticmethod - def _handle_ssh_key_issue(issue): + def _handle_ssh_key_issue(issue, instance_arn): finding = \ {"Severity": { "Product": 1, @@ -236,14 +237,14 @@ class AWSExporter(Exporter): if 'aws_instance_id' in issue: finding["Resources"] = [{ "Type": "AwsEc2Instance", - "Id": issue['aws_instance_id'] + "Id": instance_arn.format(instance_id=issue['aws_instance_id']) }] else: finding["Resources"] = [{'Type': 'Other'}] return finding @staticmethod - def _handle_elastic_issue(issue): + def _handle_elastic_issue(issue, instance_arn): finding = \ {"Severity": { "Product": 10, @@ -259,7 +260,7 @@ class AWSExporter(Exporter): if 'aws_instance_id' in issue: finding["Resources"] = [{ "Type": "AwsEc2Instance", - "Id": issue['aws_instance_id'] + "Id": instance_arn.format(instance_id=issue['aws_instance_id']) }] else: finding["Resources"] = [{'Type': 'Other'}] @@ -267,7 +268,7 @@ class AWSExporter(Exporter): return finding @staticmethod - def _handle_island_cross_segment_issue(issue): + def _handle_island_cross_segment_issue(issue, instance_arn): finding = \ {"Severity": { "Product": 1, @@ -288,7 +289,7 @@ class AWSExporter(Exporter): if 'aws_instance_id' in issue: finding["Resources"] = [{ "Type": "AwsEc2Instance", - "Id": issue['aws_instance_id'] + "Id": instance_arn.format(instance_id=issue['aws_instance_id']) }] else: finding["Resources"] = [{'Type': 'Other'}] @@ -296,7 +297,7 @@ class AWSExporter(Exporter): return finding @staticmethod - def _handle_shared_passwords_issue(issue): + def _handle_shared_passwords_issue(issue, instance_arn): finding = \ {"Severity": { "Product": 1, @@ -312,7 +313,7 @@ class AWSExporter(Exporter): if 'aws_instance_id' in issue: finding["Resources"] = [{ "Type": "AwsEc2Instance", - "Id": issue['aws_instance_id'] + "Id": instance_arn.format(instance_id=issue['aws_instance_id']) }] else: finding["Resources"] = [{'Type': 'Other'}] @@ -320,7 +321,7 @@ class AWSExporter(Exporter): return finding @staticmethod - def _handle_shellshock_issue(issue): + def _handle_shellshock_issue(issue, instance_arn): finding = \ {"Severity": { "Product": 10, @@ -337,7 +338,7 @@ class AWSExporter(Exporter): if 'aws_instance_id' in issue: finding["Resources"] = [{ "Type": "AwsEc2Instance", - "Id": issue['aws_instance_id'] + "Id": instance_arn.format(instance_id=issue['aws_instance_id']) }] else: finding["Resources"] = [{'Type': 'Other'}] @@ -345,7 +346,7 @@ class AWSExporter(Exporter): return finding @staticmethod - def _handle_smb_password_issue(issue): + def _handle_smb_password_issue(issue, instance_arn): finding = \ {"Severity": { "Product": 1, @@ -363,7 +364,7 @@ class AWSExporter(Exporter): if 'aws_instance_id' in issue: finding["Resources"] = [{ "Type": "AwsEc2Instance", - "Id": issue['aws_instance_id'] + "Id": instance_arn.format(instance_id=issue['aws_instance_id']) }] else: finding["Resources"] = [{'Type': 'Other'}] @@ -371,7 +372,7 @@ class AWSExporter(Exporter): return finding @staticmethod - def _handle_wmi_password_issue(issue): + def _handle_wmi_password_issue(issue, instance_arn): finding = \ {"Severity": { "Product": 1, @@ -389,7 +390,7 @@ class AWSExporter(Exporter): if 'aws_instance_id' in issue: finding["Resources"] = [{ "Type": "AwsEc2Instance", - "Id": issue['aws_instance_id'] + "Id": instance_arn.format(instance_id=issue['aws_instance_id']) }] else: finding["Resources"] = [{'Type': 'Other'}] @@ -397,7 +398,7 @@ class AWSExporter(Exporter): return finding @staticmethod - def _handle_wmi_pth_issue(issue): + def _handle_wmi_pth_issue(issue, instance_arn): finding = \ {"Severity": { "Product": 1, @@ -415,7 +416,7 @@ class AWSExporter(Exporter): if 'aws_instance_id' in issue: finding["Resources"] = [{ "Type": "AwsEc2Instance", - "Id": issue['aws_instance_id'] + "Id": instance_arn.format(instance_id=issue['aws_instance_id']) }] else: finding["Resources"] = [{'Type': 'Other'}] @@ -423,7 +424,7 @@ class AWSExporter(Exporter): return finding @staticmethod - def _handle_rdp_issue(issue): + def _handle_rdp_issue(issue, instance_arn): finding = \ {"Severity": { "Product": 1, @@ -441,7 +442,7 @@ class AWSExporter(Exporter): if 'aws_instance_id' in issue: finding["Resources"] = [{ "Type": "AwsEc2Instance", - "Id": issue['aws_instance_id'] + "Id": instance_arn.format(instance_id=issue['aws_instance_id']) }] else: finding["Resources"] = [{'Type': 'Other'}] @@ -449,7 +450,7 @@ class AWSExporter(Exporter): return finding @staticmethod - def _handle_shared_passwords_domain_issue(issue): + def _handle_shared_passwords_domain_issue(issue, instance_arn): finding = \ {"Severity": { "Product": 1, @@ -466,7 +467,7 @@ class AWSExporter(Exporter): if 'aws_instance_id' in issue: finding["Resources"] = [{ "Type": "AwsEc2Instance", - "Id": issue['aws_instance_id'] + "Id": instance_arn.format(instance_id=issue['aws_instance_id']) }] else: finding["Resources"] = [{'Type': 'Other'}] @@ -474,7 +475,7 @@ class AWSExporter(Exporter): return finding @staticmethod - def _handle_shared_admins_domain_issue(issue): + def _handle_shared_admins_domain_issue(issue, instance_arn): finding = \ {"Severity": { "Product": 1, @@ -492,7 +493,7 @@ class AWSExporter(Exporter): if 'aws_instance_id' in issue: finding["Resources"] = [{ "Type": "AwsEc2Instance", - "Id": issue['aws_instance_id'] + "Id": instance_arn.format(instance_id=issue['aws_instance_id']) }] else: finding["Resources"] = [{'Type': 'Other'}] @@ -500,7 +501,7 @@ class AWSExporter(Exporter): return finding @staticmethod - def _handle_strong_users_on_crit_issue(issue): + def _handle_strong_users_on_crit_issue(issue, instance_arn): finding = \ {"Severity": { "Product": 1, @@ -518,7 +519,7 @@ class AWSExporter(Exporter): if 'aws_instance_id' in issue: finding["Resources"] = [{ "Type": "AwsEc2Instance", - "Id": issue['aws_instance_id'] + "Id": instance_arn.format(instance_id=issue['aws_instance_id']) }] else: finding["Resources"] = [{'Type': 'Other'}] @@ -526,7 +527,7 @@ class AWSExporter(Exporter): return finding @staticmethod - def _handle_struts2_issue(issue): + def _handle_struts2_issue(issue, instance_arn): finding = \ {"Severity": { "Product": 10, @@ -543,7 +544,7 @@ class AWSExporter(Exporter): if 'aws_instance_id' in issue: finding["Resources"] = [{ "Type": "AwsEc2Instance", - "Id": issue['aws_instance_id'] + "Id": instance_arn.format(instance_id=issue['aws_instance_id']) }] else: finding["Resources"] = [{'Type': 'Other'}] @@ -551,7 +552,7 @@ class AWSExporter(Exporter): return finding @staticmethod - def _handle_weblogic_issue(issue): + def _handle_weblogic_issue(issue, instance_arn): finding = \ {"Severity": { "Product": 10, @@ -570,7 +571,7 @@ class AWSExporter(Exporter): if 'aws_instance_id' in issue: finding["Resources"] = [{ "Type": "AwsEc2Instance", - "Id": issue['aws_instance_id'] + "Id": instance_arn.format(instance_id=issue['aws_instance_id']) }] else: finding["Resources"] = [{'Type': 'Other'}] @@ -578,7 +579,7 @@ class AWSExporter(Exporter): return finding @staticmethod - def _handle_hadoop_issue(issue): + def _handle_hadoop_issue(issue, instance_arn): finding = \ {"Severity": { "Product": 10, @@ -594,7 +595,7 @@ class AWSExporter(Exporter): if 'aws_instance_id' in issue: finding["Resources"] = [{ "Type": "AwsEc2Instance", - "Id": issue['aws_instance_id'] + "Id": instance_arn.format(instance_id=issue['aws_instance_id']) }] else: finding["Resources"] = [{'Type': 'Other'}] From e24e9b90f7e6d327abe115b94d34eb3c862105a8 Mon Sep 17 00:00:00 2001 From: "maor.rayzin" Date: Wed, 28 Nov 2018 18:54:50 +0200 Subject: [PATCH 37/46] * Added fallback case for urllib failure to get the region * Added some safe checks for formatting and happy flows * Removed productARN from server_config.json - it will now be inserted in deb build. * Added the awscli lib to be installed via pip --- monkey/common/cloud/aws.py | 1 + monkey/monkey_island/cc/resources/aws_exporter.py | 12 ++++++++---- monkey/monkey_island/cc/server_config.json | 5 +---- .../deb-package/monkey_island_pip_requirements.txt | 1 + 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/monkey/common/cloud/aws.py b/monkey/common/cloud/aws.py index 90267bca7..7937815ef 100644 --- a/monkey/common/cloud/aws.py +++ b/monkey/common/cloud/aws.py @@ -10,6 +10,7 @@ class AWS(object): self.region = urllib2.urlopen('http://169.254.169.254/latest/meta-data/placement/availability-zone').read()[:-1] except urllib2.URLError: self.instance_id = None + self.region = None def get_instance_id(self): return self.instance_id diff --git a/monkey/monkey_island/cc/resources/aws_exporter.py b/monkey/monkey_island/cc/resources/aws_exporter.py index 412b8390a..735de6584 100644 --- a/monkey/monkey_island/cc/resources/aws_exporter.py +++ b/monkey/monkey_island/cc/resources/aws_exporter.py @@ -77,8 +77,8 @@ class AWSExporter(Exporter): } configured_product_arn = load_server_configuration_from_file()['aws'].get('sec_hub_product_arn', '') - product_arn = 'arn:aws:securityhub:{region}:{arn}'.format(region='us-west-2', arn=configured_product_arn) - instance_arn = 'arn:aws:ec2:' + region + ':instance:{instance_id}' + product_arn = 'arn:aws:securityhub:{region}:{arn}'.format(region=region, arn=configured_product_arn) + instance_arn = 'arn:aws:ec2:' + str(region) + ':instance:{instance_id}' account_id = AWSExporter._get_aws_keys().get('aws_account_id', '') finding = { @@ -98,6 +98,10 @@ class AWSExporter(Exporter): @staticmethod def _send_findings(findings_list, creds_dict, region): try: + if not creds_dict: + logger.info('No AWS access credentials received in configuration') + return False + securityhub = boto3.client('securityhub', aws_access_key_id=creds_dict.get('aws_access_key_id', ''), aws_secret_access_key=creds_dict.get('aws_secret_access_key', ''), @@ -109,10 +113,10 @@ class AWSExporter(Exporter): else: return False except UnknownServiceError as e: - logger.warning('AWS exporter called but AWS-CLI not installed') + logger.warning('AWS exporter called but AWS-CLI securityhub service is not installed') return False except Exception as e: - logger.error('AWS security hub findings failed to send.') + logger.exception('AWS security hub findings failed to send.') return False @staticmethod diff --git a/monkey/monkey_island/cc/server_config.json b/monkey/monkey_island/cc/server_config.json index 3ca292587..2d1a5995b 100644 --- a/monkey/monkey_island/cc/server_config.json +++ b/monkey/monkey_island/cc/server_config.json @@ -1,6 +1,3 @@ { - "server_config": "standard", - "aws": { - "sec_hub_product_arn": "324264561773:product/guardicore/aws-infection-monkey" - } + "server_config": "standard" } \ No newline at end of file diff --git a/monkey/monkey_island/deb-package/monkey_island_pip_requirements.txt b/monkey/monkey_island/deb-package/monkey_island_pip_requirements.txt index 7046bf231..3691ca490 100644 --- a/monkey/monkey_island/deb-package/monkey_island_pip_requirements.txt +++ b/monkey/monkey_island/deb-package/monkey_island_pip_requirements.txt @@ -15,4 +15,5 @@ ipaddress enum34 PyCrypto boto3 +awscli virtualenv \ No newline at end of file From 1339ab723f1d390929dac3f14dfb0b0b4e898f0d Mon Sep 17 00:00:00 2001 From: "maor.rayzin" Date: Thu, 29 Nov 2018 15:48:41 +0200 Subject: [PATCH 38/46] * mistaken _ with -... --- monkey/monkey_island/cc/resources/telemetry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/monkey_island/cc/resources/telemetry.py b/monkey/monkey_island/cc/resources/telemetry.py index 581cbf3dc..7425cb265 100644 --- a/monkey/monkey_island/cc/resources/telemetry.py +++ b/monkey/monkey_island/cc/resources/telemetry.py @@ -192,7 +192,7 @@ class Telemetry(flask_restful.Resource): wmi_handler = WMIHandler(monkey_id, telemetry_json['data']['wmi'], users_secrets) wmi_handler.process_and_handle_wmi_info() if 'aws' in telemetry_json['data']: - if 'instance-id' in telemetry_json['data']['aws']: + if 'instance_id' in telemetry_json['data']['aws']: mongo.db.monkey.update_one({'_id': monkey_id}, {'aws_instance_id': telemetry_json['data']['aws']['instance-id']}) From 498ddcacf510315833bbbd45c85cd92245856350 Mon Sep 17 00:00:00 2001 From: "maor.rayzin" Date: Thu, 29 Nov 2018 16:51:12 +0200 Subject: [PATCH 39/46] * mistaken _ with -... --- monkey/monkey_island/cc/resources/telemetry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/monkey_island/cc/resources/telemetry.py b/monkey/monkey_island/cc/resources/telemetry.py index 7425cb265..ac2addbb5 100644 --- a/monkey/monkey_island/cc/resources/telemetry.py +++ b/monkey/monkey_island/cc/resources/telemetry.py @@ -194,7 +194,7 @@ class Telemetry(flask_restful.Resource): if 'aws' in telemetry_json['data']: if 'instance_id' in telemetry_json['data']['aws']: mongo.db.monkey.update_one({'_id': monkey_id}, - {'aws_instance_id': telemetry_json['data']['aws']['instance-id']}) + {'aws_instance_id': telemetry_json['data']['aws']['instance_id']}) @staticmethod def add_ip_to_ssh_keys(ip, ssh_info): From 1cedfb5c2da01326c7b206f5b321f1b6260c777c Mon Sep 17 00:00:00 2001 From: "maor.rayzin" Date: Thu, 29 Nov 2018 17:43:53 +0200 Subject: [PATCH 40/46] small fixes --- monkey/monkey_island/cc/resources/telemetry.py | 2 +- monkey/monkey_island/cc/services/report.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/monkey/monkey_island/cc/resources/telemetry.py b/monkey/monkey_island/cc/resources/telemetry.py index ac2addbb5..b88acbac6 100644 --- a/monkey/monkey_island/cc/resources/telemetry.py +++ b/monkey/monkey_island/cc/resources/telemetry.py @@ -194,7 +194,7 @@ class Telemetry(flask_restful.Resource): if 'aws' in telemetry_json['data']: if 'instance_id' in telemetry_json['data']['aws']: mongo.db.monkey.update_one({'_id': monkey_id}, - {'aws_instance_id': telemetry_json['data']['aws']['instance_id']}) + {'$set': {'aws_instance_id': telemetry_json['data']['aws']['instance_id']}}) @staticmethod def add_ip_to_ssh_keys(ip, ssh_info): diff --git a/monkey/monkey_island/cc/services/report.py b/monkey/monkey_island/cc/services/report.py index 3120194a3..bd03fb78c 100644 --- a/monkey/monkey_island/cc/services/report.py +++ b/monkey/monkey_island/cc/services/report.py @@ -555,7 +555,8 @@ class ReportService: def get_machine_aws_instance_id(hostname): aws_instance_id_list = list(mongo.db.monkey.find({'hostname': hostname}, {'aws_instance_id': 1})) if aws_instance_id_list: - return str(aws_instance_id_list[0].get('aws_instance_id', None)) + if 'aws_instance_id' in aws_instance_id_list[0]: + return str(aws_instance_id_list[0]['aws_instance_id']) else: return None From 3ca761f49217380ed29f708930b4807b96f4d1f2 Mon Sep 17 00:00:00 2001 From: "maor.rayzin" Date: Tue, 11 Dec 2018 12:14:38 +0200 Subject: [PATCH 41/46] RCR: - started the report exporter manager singleton. - added region parsing using regex - --- monkey/common/cloud/aws.py | 14 ++++++- monkey/monkey_island/cc/services/report.py | 17 +++----- .../monkey_island/report_exporter_manager.py | 40 +++++++++++++++++++ monkey/monkey_island/requirements.txt | 3 +- 4 files changed, 60 insertions(+), 14 deletions(-) create mode 100644 monkey/monkey_island/report_exporter_manager.py diff --git a/monkey/common/cloud/aws.py b/monkey/common/cloud/aws.py index 7937815ef..401bbec40 100644 --- a/monkey/common/cloud/aws.py +++ b/monkey/common/cloud/aws.py @@ -1,3 +1,4 @@ +import re import urllib2 __author__ = 'itay.mizeretz' @@ -7,11 +8,22 @@ class AWS(object): def __init__(self): try: self.instance_id = urllib2.urlopen('http://169.254.169.254/latest/meta-data/instance-id').read() - self.region = urllib2.urlopen('http://169.254.169.254/latest/meta-data/placement/availability-zone').read()[:-1] + self.region = self._parse_region(urllib2.urlopen('http://169.254.169.254/latest/meta-data/placement/availability-zone').read()) except urllib2.URLError: self.instance_id = None self.region = None + @staticmethod + def _parse_region(region_url_response): + # For a list of regions: 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 get_instance_id(self): return self.instance_id diff --git a/monkey/monkey_island/cc/services/report.py b/monkey/monkey_island/cc/services/report.py index bd03fb78c..8f72e1b17 100644 --- a/monkey/monkey_island/cc/services/report.py +++ b/monkey/monkey_island/cc/services/report.py @@ -8,7 +8,6 @@ from enum import Enum from six import text_type from cc.database import mongo -from cc.environment.environment import load_env_from_file, AWS from cc.resources.aws_exporter import AWSExporter from cc.services.config import ConfigService from cc.services.edge import EdgeService @@ -677,9 +676,7 @@ class ReportService: @staticmethod def is_report_generated(): generated_report = mongo.db.report.find_one({}) - if generated_report is None: - return False - return True + return generated_report is not None @staticmethod def generate_report(): @@ -734,6 +731,10 @@ class ReportService: @staticmethod def is_latest_report_exists(): + """ + This function checks if a monkey report was already generated and if it's the latest one. + :return: True if report is the latest one, False if there isn't a report or its not the latest. + """ latest_report_doc = mongo.db.report.find_one({}, {'meta.latest_monkey_modifytime': 1}) if latest_report_doc: @@ -755,14 +756,6 @@ class ReportService: {'exploits': {'$elemMatch': {'exploiter': exploit_type, 'result': True}}}, limit=1) > 0 - @staticmethod - def get_active_exporters(): - # This function should be in another module in charge of building a list of active exporters - exporters_list = [] - if str(load_env_from_file()) == AWS: - exporters_list.append(AWSExporter) - return exporters_list - @staticmethod def export_to_exporters(report): for exporter in ReportService.get_active_exporters(): diff --git a/monkey/monkey_island/report_exporter_manager.py b/monkey/monkey_island/report_exporter_manager.py new file mode 100644 index 000000000..7e9afc8a9 --- /dev/null +++ b/monkey/monkey_island/report_exporter_manager.py @@ -0,0 +1,40 @@ +from cc.environment.environment import load_env_from_file, AWS +from cc.resources.aws_exporter import AWSExporter +import logging + +logger = logging.getLogger(__name__) + + +class Borg: + _shared_state = {} + + def __init__(self): + self.__dict__ = self._shared_state + + +class ReportExporterManager(Borg): + def __init__(self): + Borg.__init__(self) + self._exporters_list = [] + self._init_exporters() + + def get_exporters_list(self): + return self._exporters_list + + def _init_exporters(self): + self._init_aws_exporter() + + def _init_aws_exporter(self): + if str(load_env_from_file()) == AWS: + self._exporters_list.append(AWSExporter) + + def export(self): + try: + for exporter in self._exporters_list: + exporter().handle_report() + except Exception as e: + logger.exception('Failed to export report') + +if __name__ == '__main__': + print ReportExporterManager().get_exporters_list() + print ReportExporterManager().get_exporters_list() diff --git a/monkey/monkey_island/requirements.txt b/monkey/monkey_island/requirements.txt index f094df947..858642d19 100644 --- a/monkey/monkey_island/requirements.txt +++ b/monkey/monkey_island/requirements.txt @@ -14,4 +14,5 @@ netifaces ipaddress enum34 PyCrypto -boto3 \ No newline at end of file +boto3 +awscli \ No newline at end of file From 7f3ee6952758e4a95ea3c42a36f46e3a062b726e Mon Sep 17 00:00:00 2001 From: "maor.rayzin" Date: Mon, 31 Dec 2018 14:51:07 +0200 Subject: [PATCH 42/46] - Created the exporter_init file, in there the exporter manager singleton is created and populated with the relevant exporters (the aws exporter in this case) - changed the report file to use the new exporter manager singleton - changed the finding structure in the aws_exporter.py, divided it to creation functions and cleaned the code. --- monkey/monkey_island/cc/exporter_init.py | 17 + monkey/monkey_island/cc/main.py | 3 + .../cc/report_exporter_manager.py | 32 + .../cc/resources/aws_exporter.py | 613 ++++++------------ monkey/monkey_island/cc/services/report.py | 9 +- .../monkey_island/report_exporter_manager.py | 40 -- 6 files changed, 261 insertions(+), 453 deletions(-) create mode 100644 monkey/monkey_island/cc/exporter_init.py create mode 100644 monkey/monkey_island/cc/report_exporter_manager.py delete mode 100644 monkey/monkey_island/report_exporter_manager.py diff --git a/monkey/monkey_island/cc/exporter_init.py b/monkey/monkey_island/cc/exporter_init.py new file mode 100644 index 000000000..c2285772e --- /dev/null +++ b/monkey/monkey_island/cc/exporter_init.py @@ -0,0 +1,17 @@ +from cc.environment.environment import load_env_from_file, AWS +from cc.report_exporter_manager import ReportExporterManager +from cc.resources.aws_exporter import AWSExporter + + +def populate_exporter_list(): + + manager = ReportExporterManager() + if is_aws_exporter_required(): + manager.add_exporter_to_list(AWSExporter) + + +def is_aws_exporter_required(): + if str(load_env_from_file()) == AWS: + return True + else: + return False diff --git a/monkey/monkey_island/cc/main.py b/monkey/monkey_island/cc/main.py index 86015b5d4..a6ded6628 100644 --- a/monkey/monkey_island/cc/main.py +++ b/monkey/monkey_island/cc/main.py @@ -34,6 +34,8 @@ def main(): logger.info('Waiting for MongoDB server') time.sleep(1) + + app = init_app(mongo_url) if env.is_debug(): app.run(host='0.0.0.0', debug=True, ssl_context=('monkey_island/cc/server.crt', 'monkey_island/cc/server.key')) @@ -44,6 +46,7 @@ def main(): http_server.listen(env.get_island_port()) logger.info( 'Monkey Island Server is running on https://{}:{}'.format(local_ip_addresses()[0], env.get_island_port())) + IOLoop.instance().start() diff --git a/monkey/monkey_island/cc/report_exporter_manager.py b/monkey/monkey_island/cc/report_exporter_manager.py new file mode 100644 index 000000000..210f28966 --- /dev/null +++ b/monkey/monkey_island/cc/report_exporter_manager.py @@ -0,0 +1,32 @@ +import logging + +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 + + 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): + try: + for exporter in self._exporters_set: + exporter().handle_report(report) + except Exception as e: + logger.exception('Failed to export report') diff --git a/monkey/monkey_island/cc/resources/aws_exporter.py b/monkey/monkey_island/cc/resources/aws_exporter.py index 735de6584..8890342dd 100644 --- a/monkey/monkey_island/cc/resources/aws_exporter.py +++ b/monkey/monkey_island/cc/resources/aws_exporter.py @@ -87,6 +87,7 @@ class AWSExporter(Exporter): "ProductArn": product_arn, "GeneratorId": issue['type'], "AwsAccountId": account_id, + "RecordState": "ACTIVE", "Types": [ "Software and Configuration Checks/Vulnerabilities/CVE" ], @@ -120,488 +121,288 @@ class AWSExporter(Exporter): return False @staticmethod - def _handle_tunnel_issue(issue, instance_arn): - finding = \ - {"Severity": { - "Product": 5, - "Normalized": 100 - }, "RecordState": "ACTIVE", - "Title": "Weak segmentation - Machines were able to communicate over unused ports.", - "Description": "Use micro-segmentation policies to disable communication other than the required.", - "Remediation": { - "Recommendation": { - "Text": "Machines are not locked down at port level. Network tunnel was set up from {0} to {1}" - .format(issue['machine'], issue['dest']) - } - }} - - if 'aws_instance_id' in issue: - finding["Resources"] = [{ + def _get_finding_resource(instance_id, instance_arn): + if instance_id: + return [{ "Type": "AwsEc2Instance", - "Id": instance_arn.format(instance_id=issue['aws_instance_id']) + "Id": instance_arn.format(instance_id=instance_id) }] else: - finding["Resources"] = [{'Type': 'Other'}] + return [{'Type': 'Other'}] + + @staticmethod + def _build_generic_finding(severity, title, description, recommendation, instance_arn, instance_id=None): + finding = { + "Severity": { + "Product": severity, + "Normalized": 100 + }, + 'Resource': 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 + ) + @staticmethod def _handle_sambacry_issue(issue, instance_arn): - finding = \ - {"Severity": { - "Product": 10, - "Normalized": 100 - }, "RecordState": "ACTIVE", "Title": "Samba servers are vulnerable to 'SambaCry'", - "Description": "Change {0} password to a complex one-use password that is not shared with other computers on the network. Update your Samba server to 4.4.14 and up, 4.5.10 and up, or 4.6.4 and up." \ - .format(issue['username']), "Remediation": { - "Recommendation": { - "Text": "The machine {0} ({1}) is vulnerable to a SambaCry attack. The Monkey authenticated over the SMB protocol with user {2} and its password, and used the SambaCry vulnerability.".format( - issue['machine'], issue['ip_address'], issue['username']) - } - }} - if 'aws_instance_id' in issue: - finding["Resources"] = [{ - "Type": "AwsEc2Instance", - "Id": instance_arn.format(instance_id=issue['aws_instance_id']) - }] - else: - finding["Resources"] = [{'Type': 'Other'}] - - return finding + return AWSExporter._build_generic_finding( + severity=10, + title="Samba servers are vulnerable to 'SambaCry'", + description="Change {0} password to a complex one-use password that is not shared with other computers on the network. Update your Samba server to 4.4.14 and up, 4.5.10 and up, or 4.6.4 and up." \ + .format(issue['username']), + recommendation="The machine {0} ({1}) is vulnerable to a SambaCry attack. The Monkey authenticated over the SMB protocol with user {2} and its password, and used the SambaCry vulnerability.".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_smb_pth_issue(issue, instance_arn): - finding = \ - {"Severity": { - "Product": 5, - "Normalized": 100 - }, "RecordState": "ACTIVE", - "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']), "Remediation": { - "Recommendation": { - "Text": "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']) - } - }} - if 'aws_instance_id' in issue: - finding["Resources"] = [{ - "Type": "AwsEc2Instance", - "Id": instance_arn.format(instance_id=issue['aws_instance_id']) - }] - else: - finding["Resources"] = [{'Type': 'Other'}] - - return finding + 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 + ) @staticmethod def _handle_ssh_issue(issue, instance_arn): - finding = \ - {"Severity": { - "Product": 1, - "Normalized": 100 - }, "RecordState": "ACTIVE", - "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']), "Remediation": { - "Recommendation": { - "Text": "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']) - } - }} - if 'aws_instance_id' in issue: - finding["Resources"] = [{ - "Type": "AwsEc2Instance", - "Id": instance_arn.format(instance_id=issue['aws_instance_id']) - }] - else: - finding["Resources"] = [{'Type': 'Other'}] - - 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="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 + ) @staticmethod def _handle_ssh_key_issue(issue, instance_arn): - finding = \ - {"Severity": { - "Product": 1, - "Normalized": 100 - }, "RecordState": "ACTIVE", - "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']), - "Remediation": { - "Recommendation": { - "Text": "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']) - } - }} - if 'aws_instance_id' in issue: - finding["Resources"] = [{ - "Type": "AwsEc2Instance", - "Id": instance_arn.format(instance_id=issue['aws_instance_id']) - }] - else: - finding["Resources"] = [{'Type': 'Other'}] - 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_elastic_issue(issue, instance_arn): - finding = \ - {"Severity": { - "Product": 10, - "Normalized": 100 - }, "RecordState": "ACTIVE", "Title": "Elasticsearch servers are vulnerable to CVE-2015-1427", - "Description": "Update your Elastic Search server to version 1.4.3 and up.", "Remediation": { - "Recommendation": { - "Text": "The machine {0}({1}) is vulnerable to an Elastic Groovy attack. The attack was made possible because the Elastic Search server was not patched against CVE-2015-1427.".format( - issue['machine'], issue['ip_address']) - } - }} - if 'aws_instance_id' in issue: - finding["Resources"] = [{ - "Type": "AwsEc2Instance", - "Id": instance_arn.format(instance_id=issue['aws_instance_id']) - }] - else: - finding["Resources"] = [{'Type': 'Other'}] - - return finding + return AWSExporter._build_generic_finding( + severity=10, + title="Elastic Search servers are vulnerable to CVE-2015-1427", + description="Update your Elastic Search server to version 1.4.3 and up.", + recommendation="The machine {0}({1}) is vulnerable to an Elastic Groovy attack. The attack was made possible because the Elastic Search server was not patched against CVE-2015-1427.".format( + issue['machine'], issue['ip_address']), + 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): - finding = \ - {"Severity": { - "Product": 1, - "Normalized": 100 - }, "RecordState": "ACTIVE", - "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.", - "Remediation": { - "Recommendation": { - "Text": "The network can probably be segmented. A monkey instance on \ + + 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']) - } - }} - - if 'aws_instance_id' in issue: - finding["Resources"] = [{ - "Type": "AwsEc2Instance", - "Id": instance_arn.format(instance_id=issue['aws_instance_id']) - }] - else: - finding["Resources"] = [{'Type': 'Other'}] - - return finding + 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): - finding = \ - {"Severity": { - "Product": 1, - "Normalized": 100 - }, "RecordState": "ACTIVE", "Title": "Multiple users have the same password", - "Description": "Some users are sharing passwords, this should be fixed by changing passwords.", - "Remediation": { - "Recommendation": { - "Text": "These users are sharing access password: {0}.".format(issue['shared_with']) - } - }} - if 'aws_instance_id' in issue: - finding["Resources"] = [{ - "Type": "AwsEc2Instance", - "Id": instance_arn.format(instance_id=issue['aws_instance_id']) - }] - else: - finding["Resources"] = [{'Type': 'Other'}] - - return finding + 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_shellshock_issue(issue, instance_arn): - finding = \ - {"Severity": { - "Product": 10, - "Normalized": 100 - }, "RecordState": "ACTIVE", "Title": "Machines are vulnerable to 'Shellshock'", - "Description": "Update your Bash to a ShellShock-patched version.", "Remediation": { - "Recommendation": { - "Text": "The machine {0} ({1}) is vulnerable to a ShellShock attack. " - "The attack was made possible because the HTTP server running on TCP port {2} was vulnerable to a shell injection attack on the paths: {3}.".format( - issue['machine'], issue['ip_address'], issue['port'], issue['paths']) - } - }} - if 'aws_instance_id' in issue: - finding["Resources"] = [{ - "Type": "AwsEc2Instance", - "Id": instance_arn.format(instance_id=issue['aws_instance_id']) - }] - else: - finding["Resources"] = [{'Type': 'Other'}] - - return finding + return AWSExporter._build_generic_finding( + severity=10, + title="Machines are vulnerable to 'Shellshock'", + description="Update your Bash to a ShellShock-patched version.", + recommendation="The machine {0} ({1}) is vulnerable to a ShellShock attack. " + "The attack was made possible because the HTTP server running on TCP port {2} was vulnerable to a shell injection attack on the paths: {3}.".format( + issue['machine'], issue['ip_address'], issue['port'], issue['paths']), + 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): - finding = \ - {"Severity": { - "Product": 1, - "Normalized": 100 - }, "RecordState": "ACTIVE", - "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']), "Remediation": { - "Recommendation": { - "Text": "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']) - } - }} - if 'aws_instance_id' in issue: - finding["Resources"] = [{ - "Type": "AwsEc2Instance", - "Id": instance_arn.format(instance_id=issue['aws_instance_id']) - }] - else: - finding["Resources"] = [{'Type': 'Other'}] - - return finding + 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): - finding = \ - {"Severity": { - "Product": 1, - "Normalized": 100 - }, "RecordState": "ACTIVE", - "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.", - "Remediation": { - "Recommendation": { - "Text": "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']) - } - }} - if 'aws_instance_id' in issue: - finding["Resources"] = [{ - "Type": "AwsEc2Instance", - "Id": instance_arn.format(instance_id=issue['aws_instance_id']) - }] - else: - finding["Resources"] = [{'Type': 'Other'}] - - return finding + 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): - finding = \ - {"Severity": { - "Product": 1, - "Normalized": 100 - }, "RecordState": "ACTIVE", - "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']), "Remediation": { - "Recommendation": { - "Text": "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']) - } - }} - if 'aws_instance_id' in issue: - finding["Resources"] = [{ - "Type": "AwsEc2Instance", - "Id": instance_arn.format(instance_id=issue['aws_instance_id']) - }] - else: - finding["Resources"] = [{'Type': 'Other'}] - - return finding + 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_rdp_issue(issue, instance_arn): - finding = \ - {"Severity": { - "Product": 1, - "Normalized": 100 - }, "RecordState": "ACTIVE", - "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']), "Remediation": { - "Recommendation": { - "Text": "The machine machine ({ip_address}) is vulnerable to a RDP attack. The Monkey authenticated over the RDP protocol with user {username} and its password.".format( - machine=issue['machine'], ip_address=issue['ip_address'], username=issue['username']) - } - }} - if 'aws_instance_id' in issue: - finding["Resources"] = [{ - "Type": "AwsEc2Instance", - "Id": instance_arn.format(instance_id=issue['aws_instance_id']) - }] - else: - finding["Resources"] = [{'Type': 'Other'}] - - return finding + 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 RDP attack. The Monkey authenticated over the RDP 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_shared_passwords_domain_issue(issue, instance_arn): - finding = \ - {"Severity": { - "Product": 1, - "Normalized": 100 - }, "RecordState": "ACTIVE", "Title": "Multiple users have the same password.", - "Description": "Some domain users are sharing passwords, this should be fixed by changing passwords.", - "Remediation": { - "Recommendation": { - "Text": "These users are sharing access password: {shared_with}.".format( - shared_with=issue['shared_with']) - } - }} - if 'aws_instance_id' in issue: - finding["Resources"] = [{ - "Type": "AwsEc2Instance", - "Id": instance_arn.format(instance_id=issue['aws_instance_id']) - }] - else: - finding["Resources"] = [{'Type': 'Other'}] - - return finding + 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): - finding = \ - {"Severity": { - "Product": 1, - "Normalized": 100 - }, "RecordState": "ACTIVE", - "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.", - "Remediation": { - "Recommendation": { - "Text": "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']) - } - }} - if 'aws_instance_id' in issue: - finding["Resources"] = [{ - "Type": "AwsEc2Instance", - "Id": instance_arn.format(instance_id=issue['aws_instance_id']) - }] - else: - finding["Resources"] = [{'Type': 'Other'}] - - return finding + 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): - finding = \ - {"Severity": { - "Product": 1, - "Normalized": 100 - }, "RecordState": "ACTIVE", - "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.", - "Remediation": { - "Recommendation": { - "Text": "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']) - } - }} - if 'aws_instance_id' in issue: - finding["Resources"] = [{ - "Type": "AwsEc2Instance", - "Id": instance_arn.format(instance_id=issue['aws_instance_id']) - }] - else: - finding["Resources"] = [{'Type': 'Other'}] - - return finding + 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_struts2_issue(issue, instance_arn): - finding = \ - {"Severity": { - "Product": 10, - "Normalized": 100 - }, "RecordState": "ACTIVE", "Title": "Struts2 servers are vulnerable to remote code execution.", - "Description": "Upgrade Struts2 to version 2.3.32 or 2.5.10.1 or any later versions.", "Remediation": { - "Recommendation": { - "Text": "Struts2 server at {machine} ({ip_address}) is vulnerable to remote code execution attack." - " The attack was made possible because the server is using an old version of Jakarta based file upload Multipart parser.".format( - machine=issue['machine'], ip_address=issue['ip_address']) - } - }} - if 'aws_instance_id' in issue: - finding["Resources"] = [{ - "Type": "AwsEc2Instance", - "Id": instance_arn.format(instance_id=issue['aws_instance_id']) - }] - else: - finding["Resources"] = [{'Type': 'Other'}] - - return finding + return AWSExporter._build_generic_finding( + severity=10, + title="Struts2 servers are vulnerable to remote code execution.", + description="Upgrade Struts2 to version 2.3.32 or 2.5.10.1 or any later versions.", + recommendation="Struts2 server at {machine} ({ip_address}) is vulnerable to remote code execution attack." + " The attack was made possible because the server is using an old version of Jakarta based file upload Multipart parser.".format( + machine=issue['machine'], ip_address=issue['ip_address']), + instance_arn=instance_arn, + instance_id=issue['aws_instance_id'] if 'aws_instance_id' in issue else None + ) @staticmethod def _handle_weblogic_issue(issue, instance_arn): - finding = \ - {"Severity": { - "Product": 10, - "Normalized": 100 - }, "RecordState": "ACTIVE", "Title": "Oracle WebLogic servers are vulnerable to remote code execution.", - "Description": "Install Oracle critical patch updates. Or update to the latest version. " \ - "Vulnerable versions are 10.3.6.0.0, 12.1.3.0.0, 12.2.1.1.0 and 12.2.1.2.0.", - "Remediation": { - "Recommendation": { - "Text": "Oracle WebLogic server at {machine} ({ip_address}) is vulnerable to remote code execution attack." - " The attack was made possible due to incorrect permission assignment in Oracle Fusion Middleware (subcomponent: WLS Security).".format( - machine=issue['machine'], ip_address=issue['ip_address']) - } - }} - if 'aws_instance_id' in issue: - finding["Resources"] = [{ - "Type": "AwsEc2Instance", - "Id": instance_arn.format(instance_id=issue['aws_instance_id']) - }] - else: - finding["Resources"] = [{'Type': 'Other'}] - - return finding + return AWSExporter._build_generic_finding( + severity=10, + title="Oracle WebLogic servers are vulnerable to remote code execution.", + description="Install Oracle critical patch updates. Or update to the latest version. " \ + "Vulnerable versions are 10.3.6.0.0, 12.1.3.0.0, 12.2.1.1.0 and 12.2.1.2.0.", + recommendation="Oracle WebLogic server at {machine} ({ip_address}) is vulnerable to remote code execution attack." + " The attack was made possible due to incorrect permission assignment in Oracle Fusion Middleware (subcomponent: WLS Security).".format( + machine=issue['machine'], ip_address=issue['ip_address']), + 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): - finding = \ - {"Severity": { - "Product": 10, - "Normalized": 100 - }, "RecordState": "ACTIVE", "Title": "Hadoop/Yarn servers are vulnerable to remote code execution.", - "Description": "Run Hadoop in secure mode, add Kerberos authentication.", "Remediation": { - "Recommendation": { - "Text": "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." - } - }} - if 'aws_instance_id' in issue: - finding["Resources"] = [{ - "Type": "AwsEc2Instance", - "Id": instance_arn.format(instance_id=issue['aws_instance_id']) - }] - else: - finding["Resources"] = [{'Type': 'Other'}] - - return finding + 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 + ) diff --git a/monkey/monkey_island/cc/services/report.py b/monkey/monkey_island/cc/services/report.py index 8f72e1b17..8861e8d85 100644 --- a/monkey/monkey_island/cc/services/report.py +++ b/monkey/monkey_island/cc/services/report.py @@ -8,7 +8,7 @@ from enum import Enum from six import text_type from cc.database import mongo -from cc.resources.aws_exporter import AWSExporter +from cc.report_exporter_manager import ReportExporterManager from cc.services.config import ConfigService from cc.services.edge import EdgeService from cc.services.node import NodeService @@ -723,7 +723,7 @@ class ReportService: 'latest_monkey_modifytime': monkey_latest_modify_time } } - ReportService.export_to_exporters(report) + ReportExporterManager().export(report) mongo.db.report.drop() mongo.db.report.insert_one(report) @@ -755,8 +755,3 @@ class ReportService: return mongo.db.edge.count( {'exploits': {'$elemMatch': {'exploiter': exploit_type, 'result': True}}}, limit=1) > 0 - - @staticmethod - def export_to_exporters(report): - for exporter in ReportService.get_active_exporters(): - exporter.handle_report(report) diff --git a/monkey/monkey_island/report_exporter_manager.py b/monkey/monkey_island/report_exporter_manager.py deleted file mode 100644 index 7e9afc8a9..000000000 --- a/monkey/monkey_island/report_exporter_manager.py +++ /dev/null @@ -1,40 +0,0 @@ -from cc.environment.environment import load_env_from_file, AWS -from cc.resources.aws_exporter import AWSExporter -import logging - -logger = logging.getLogger(__name__) - - -class Borg: - _shared_state = {} - - def __init__(self): - self.__dict__ = self._shared_state - - -class ReportExporterManager(Borg): - def __init__(self): - Borg.__init__(self) - self._exporters_list = [] - self._init_exporters() - - def get_exporters_list(self): - return self._exporters_list - - def _init_exporters(self): - self._init_aws_exporter() - - def _init_aws_exporter(self): - if str(load_env_from_file()) == AWS: - self._exporters_list.append(AWSExporter) - - def export(self): - try: - for exporter in self._exporters_list: - exporter().handle_report() - except Exception as e: - logger.exception('Failed to export report') - -if __name__ == '__main__': - print ReportExporterManager().get_exporters_list() - print ReportExporterManager().get_exporters_list() From 4b06c1e3f4a2671f258d3032b0f965ee1703ad45 Mon Sep 17 00:00:00 2001 From: "maor.rayzin" Date: Mon, 31 Dec 2018 14:58:14 +0200 Subject: [PATCH 43/46] - added 'author' to each file. --- monkey/monkey_island/cc/exporter_init.py | 1 + monkey/monkey_island/cc/report_exporter_manager.py | 2 ++ monkey/monkey_island/cc/resources/aws_exporter.py | 3 +++ 3 files changed, 6 insertions(+) diff --git a/monkey/monkey_island/cc/exporter_init.py b/monkey/monkey_island/cc/exporter_init.py index c2285772e..0fc32fccb 100644 --- a/monkey/monkey_island/cc/exporter_init.py +++ b/monkey/monkey_island/cc/exporter_init.py @@ -2,6 +2,7 @@ from cc.environment.environment import load_env_from_file, AWS from cc.report_exporter_manager import ReportExporterManager from cc.resources.aws_exporter import AWSExporter +__author__ = 'maor.rayzin' def populate_exporter_list(): diff --git a/monkey/monkey_island/cc/report_exporter_manager.py b/monkey/monkey_island/cc/report_exporter_manager.py index 210f28966..a6a983a20 100644 --- a/monkey/monkey_island/cc/report_exporter_manager.py +++ b/monkey/monkey_island/cc/report_exporter_manager.py @@ -1,5 +1,7 @@ import logging +__author__ = 'maor.rayzin' + logger = logging.getLogger(__name__) diff --git a/monkey/monkey_island/cc/resources/aws_exporter.py b/monkey/monkey_island/cc/resources/aws_exporter.py index 8890342dd..0c1d51d1a 100644 --- a/monkey/monkey_island/cc/resources/aws_exporter.py +++ b/monkey/monkey_island/cc/resources/aws_exporter.py @@ -9,6 +9,9 @@ from cc.services.config import ConfigService from cc.environment.environment import load_server_configuration_from_file from common.cloud.aws import AWS +__author__ = 'maor.rayzin' + + logger = logging.getLogger(__name__) AWS_CRED_CONFIG_KEYS = [['cnc', 'aws_config', 'aws_access_key_id'], From 985f45d8de668e6f470643b652a46da7ad0a19e8 Mon Sep 17 00:00:00 2001 From: "maor.rayzin" Date: Wed, 2 Jan 2019 14:26:36 +0200 Subject: [PATCH 44/46] - Added exporters list population - some pep8 - Added a report json cleanup for mongo insertion, sometimes machine names are used as keys and these names might contain '.' which mongodb doesn't allow. - Fixed a typo and aws sec hub protocol requirements --- monkey/monkey_island/cc/exporter_init.py | 1 + monkey/monkey_island/cc/main.py | 4 +-- .../cc/resources/aws_exporter.py | 4 +-- .../cc/services/config_schema.py | 28 +++++++++++++++++-- monkey/monkey_island/cc/services/report.py | 16 ++++++++++- 5 files changed, 46 insertions(+), 7 deletions(-) diff --git a/monkey/monkey_island/cc/exporter_init.py b/monkey/monkey_island/cc/exporter_init.py index 0fc32fccb..9b25469f9 100644 --- a/monkey/monkey_island/cc/exporter_init.py +++ b/monkey/monkey_island/cc/exporter_init.py @@ -4,6 +4,7 @@ from cc.resources.aws_exporter import AWSExporter __author__ = 'maor.rayzin' + def populate_exporter_list(): manager = ReportExporterManager() diff --git a/monkey/monkey_island/cc/main.py b/monkey/monkey_island/cc/main.py index a6ded6628..713e83b96 100644 --- a/monkey/monkey_island/cc/main.py +++ b/monkey/monkey_island/cc/main.py @@ -18,6 +18,7 @@ json_setup_logging(default_path=os.path.join(BASE_PATH, 'cc', 'island_logger_def logger = logging.getLogger(__name__) from cc.app import init_app +from cc.exporter_init import populate_exporter_list from cc.utils import local_ip_addresses from cc.environment.environment import env from cc.database import is_db_server_up @@ -34,8 +35,7 @@ def main(): logger.info('Waiting for MongoDB server') time.sleep(1) - - + populate_exporter_list() app = init_app(mongo_url) if env.is_debug(): app.run(host='0.0.0.0', debug=True, ssl_context=('monkey_island/cc/server.crt', 'monkey_island/cc/server.key')) diff --git a/monkey/monkey_island/cc/resources/aws_exporter.py b/monkey/monkey_island/cc/resources/aws_exporter.py index 0c1d51d1a..bd6ef3a10 100644 --- a/monkey/monkey_island/cc/resources/aws_exporter.py +++ b/monkey/monkey_island/cc/resources/aws_exporter.py @@ -131,7 +131,7 @@ class AWSExporter(Exporter): "Id": instance_arn.format(instance_id=instance_id) }] else: - return [{'Type': 'Other'}] + return [{'Type': 'Other', 'Id': 'None'}] @staticmethod def _build_generic_finding(severity, title, description, recommendation, instance_arn, instance_id=None): @@ -140,7 +140,7 @@ class AWSExporter(Exporter): "Product": severity, "Normalized": 100 }, - 'Resource': AWSExporter._get_finding_resource(instance_id, instance_arn), + 'Resources': AWSExporter._get_finding_resource(instance_id, instance_arn), "Title": title, "Description": description, "Remediation": { diff --git a/monkey/monkey_island/cc/services/config_schema.py b/monkey/monkey_island/cc/services/config_schema.py index d4d294afc..bb5b10cbb 100644 --- a/monkey/monkey_island/cc/services/config_schema.py +++ b/monkey/monkey_island/cc/services/config_schema.py @@ -1,6 +1,5 @@ WARNING_SIGN = u" \u26A0" - SCHEMA = { "title": "Monkey", "type": "object", @@ -624,6 +623,31 @@ SCHEMA = { "description": "The current command server the monkey is communicating with" } } + }, + 'aws_config': { + 'title': 'AWS Configuration', + 'type': 'object', + 'description': 'These credentials will be used in order to export the monkey\'s findings to the AWS Security Hub.', + 'properties': { + 'aws_account_id': { + 'title': 'AWS account ID', + 'type': 'string', + 'description': 'Your AWS account ID that is subscribed to security hub feeds', + 'default': '' + }, + 'aws_access_key_id': { + 'title': 'AWS access key ID', + 'type': 'string', + 'description': 'Your AWS public access key ID, can be found in the IAM user interface in the AWS console.', + 'default': '' + }, + 'aws_secret_access_key': { + 'title': 'AWS secret access key', + 'type': 'string', + 'description': 'Your AWS secret access key id, you can get this after creating a public access key in the console.', + 'default': '' + } + } } } }, @@ -852,4 +876,4 @@ SCHEMA = { "options": { "collapsed": True } -} \ No newline at end of file +} diff --git a/monkey/monkey_island/cc/services/report.py b/monkey/monkey_island/cc/services/report.py index 8861e8d85..a9edbaf48 100644 --- a/monkey/monkey_island/cc/services/report.py +++ b/monkey/monkey_island/cc/services/report.py @@ -3,6 +3,8 @@ import functools import ipaddress import logging + +from bson import json_util from enum import Enum from six import text_type @@ -725,10 +727,22 @@ class ReportService: } ReportExporterManager().export(report) mongo.db.report.drop() - mongo.db.report.insert_one(report) + mongo.db.report.insert_one(ReportService.clean_report_before_mongo_insert(report)) return report + @staticmethod + def clean_report_before_mongo_insert(report_dict): + """ + mongodb doesn't allow for '.' and '$' in a key's name, this function replaces the '.' char with the unicode + \u002E char instead. + :return: + """ + report_as_json = json_util.dumps(report_dict) + report_as_json.replace('.', '\u002E') + return json_util.loads(report_as_json) + + @staticmethod def is_latest_report_exists(): """ From 078470e2575cff48f642d255d2dab3fb0cd7e9cb Mon Sep 17 00:00:00 2001 From: "maor.rayzin" Date: Wed, 2 Jan 2019 16:25:26 +0200 Subject: [PATCH 45/46] - added char conversion for mongo insertion, mongodb doesn't allow for '.' in keys names and sometimes machine names might include '.' char in them. We encode with ',,,' and decode back to '.'. --- monkey/monkey_island/cc/services/report.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/monkey/monkey_island/cc/services/report.py b/monkey/monkey_island/cc/services/report.py index a9edbaf48..46a4ee448 100644 --- a/monkey/monkey_island/cc/services/report.py +++ b/monkey/monkey_island/cc/services/report.py @@ -727,19 +727,18 @@ class ReportService: } ReportExporterManager().export(report) mongo.db.report.drop() - mongo.db.report.insert_one(ReportService.clean_report_before_mongo_insert(report)) + mongo.db.report.insert_one(ReportService.encode_dot_char_before_mongo_insert(report)) return report @staticmethod - def clean_report_before_mongo_insert(report_dict): + def encode_dot_char_before_mongo_insert(report_dict): """ mongodb doesn't allow for '.' and '$' in a key's name, this function replaces the '.' char with the unicode - \u002E char instead. - :return: + ,,, combo instead. + :return: dict with formatted keys with no dots. """ - report_as_json = json_util.dumps(report_dict) - report_as_json.replace('.', '\u002E') + report_as_json = json_util.dumps(report_dict).replace('.', ',,,') return json_util.loads(report_as_json) @@ -758,10 +757,19 @@ class ReportService: return False + @staticmethod + def decode_dot_char_before_mongo_insert(report_dict): + """ + this function replaces the ',,,' combo with the '.' char instead. + :return: report dict with formatted keys (',,,' -> '.') + """ + report_as_json = json_util.dumps(report_dict).replace(',,,', '.') + return json_util.loads(report_as_json) + @staticmethod def get_report(): if ReportService.is_latest_report_exists(): - return mongo.db.report.find_one() + return ReportService.decode_dot_char_before_mongo_insert(mongo.db.report.find_one()) return ReportService.generate_report() @staticmethod From 0feb19ede52f644eb2fbfaa1d4863d1571ebd42b Mon Sep 17 00:00:00 2001 From: Daniel Goldberg Date: Sat, 26 Jan 2019 19:42:35 +0200 Subject: [PATCH 46/46] PEP8 stuff --- monkey/common/cloud/aws.py | 3 ++- monkey/monkey_island/cc/resources/exporter.py | 4 +--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/monkey/common/cloud/aws.py b/monkey/common/cloud/aws.py index afb69c1fe..2b539de67 100644 --- a/monkey/common/cloud/aws.py +++ b/monkey/common/cloud/aws.py @@ -8,7 +8,8 @@ class AWS(object): def __init__(self): try: self.instance_id = urllib2.urlopen('http://169.254.169.254/latest/meta-data/instance-id', timeout=2).read() - self.region = self._parse_region(urllib2.urlopen('http://169.254.169.254/latest/meta-data/placement/availability-zone').read()) + self.region = self._parse_region( + urllib2.urlopen('http://169.254.169.254/latest/meta-data/placement/availability-zone').read()) except urllib2.URLError: self.instance_id = None self.region = None diff --git a/monkey/monkey_island/cc/resources/exporter.py b/monkey/monkey_island/cc/resources/exporter.py index 1cf0c1b10..e79fabc07 100644 --- a/monkey/monkey_island/cc/resources/exporter.py +++ b/monkey/monkey_island/cc/resources/exporter.py @@ -1,6 +1,4 @@ - -class Exporter: - +class Exporter(object): def __init__(self): pass