From 148ee3f0f0afc0ec42aceebf41582ad64d1edb7a Mon Sep 17 00:00:00 2001 From: "maor.rayzin" Date: Sun, 25 Nov 2018 12:39:47 +0200 Subject: [PATCH] * 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