Merge pull request #221 from guardicore/feature/report_exporters
Feature/report exporters
This commit is contained in:
commit
10c88c0a41
|
@ -1,3 +1,4 @@
|
|||
import re
|
||||
import urllib2
|
||||
|
||||
__author__ = 'itay.mizeretz'
|
||||
|
@ -7,11 +8,28 @@ 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())
|
||||
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
|
||||
|
||||
def get_region(self):
|
||||
return self.region
|
||||
|
||||
def is_aws_instance(self):
|
||||
return self.instance_id is not None
|
||||
|
|
|
@ -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/<string:monkey_guid>')
|
||||
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/<string:path>')
|
||||
api.add_resource(NetMap, '/api/netmap', '/api/netmap/')
|
||||
|
|
|
@ -8,11 +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()
|
||||
|
||||
def _get_region(self):
|
||||
return self.aws_info.get_region()
|
||||
|
||||
def is_auth_enabled(self):
|
||||
return True
|
||||
|
|
|
@ -5,6 +5,8 @@ import aws
|
|||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
AWS = 'aws'
|
||||
STANDARD = 'standard'
|
||||
|
||||
ENV_DICT = {
|
||||
'standard': standard.StandardEnvironment,
|
||||
|
@ -12,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]()
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
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():
|
||||
|
||||
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
|
|
@ -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,6 +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'))
|
||||
|
@ -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()
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
import logging
|
||||
|
||||
__author__ = 'maor.rayzin'
|
||||
|
||||
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')
|
|
@ -0,0 +1,411 @@
|
|||
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
|
||||
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'],
|
||||
['cnc', 'aws_config', 'aws_secret_access_key'],
|
||||
['cnc', 'aws_config', 'aws_account_id']]
|
||||
|
||||
|
||||
class AWSExporter(Exporter):
|
||||
|
||||
@staticmethod
|
||||
def handle_report(report_json):
|
||||
aws = AWS()
|
||||
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]:
|
||||
if issue.get('aws_instance_id', None):
|
||||
findings_list.append(AWSExporter._prepare_finding(issue, aws.get_region()))
|
||||
|
||||
if not AWSExporter._send_findings(findings_list, AWSExporter._get_aws_keys(), aws.get_region()):
|
||||
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]] = str(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
|
||||
z.update(y) # modifies z with y's keys and values & returns None
|
||||
return z
|
||||
|
||||
@staticmethod
|
||||
def _prepare_finding(issue, region):
|
||||
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,
|
||||
'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
|
||||
}
|
||||
|
||||
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)
|
||||
instance_arn = 'arn:aws:ec2:' + str(region) + ':instance:{instance_id}'
|
||||
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": account_id,
|
||||
"RecordState": "ACTIVE",
|
||||
"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, instance_arn))
|
||||
|
||||
@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', ''),
|
||||
region_name=region)
|
||||
|
||||
import_response = securityhub.batch_import_findings(Findings=findings_list)
|
||||
if import_response['ResponseMetadata']['HTTPStatusCode'] == 200:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
except UnknownServiceError as e:
|
||||
logger.warning('AWS exporter called but AWS-CLI securityhub service is not installed')
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.exception('AWS security hub findings failed to send.')
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def _get_finding_resource(instance_id, instance_arn):
|
||||
if instance_id:
|
||||
return [{
|
||||
"Type": "AwsEc2Instance",
|
||||
"Id": instance_arn.format(instance_id=instance_id)
|
||||
}]
|
||||
else:
|
||||
return [{'Type': 'Other', 'Id': 'None'}]
|
||||
|
||||
@staticmethod
|
||||
def _build_generic_finding(severity, title, description, recommendation, instance_arn, instance_id=None):
|
||||
finding = {
|
||||
"Severity": {
|
||||
"Product": severity,
|
||||
"Normalized": 100
|
||||
},
|
||||
'Resources': AWSExporter._get_finding_resource(instance_id, instance_arn),
|
||||
"Title": title,
|
||||
"Description": description,
|
||||
"Remediation": {
|
||||
"Recommendation": {
|
||||
"Text": recommendation
|
||||
}
|
||||
}}
|
||||
|
||||
return finding
|
||||
|
||||
@staticmethod
|
||||
def _handle_tunnel_issue(issue, instance_arn):
|
||||
|
||||
return AWSExporter._build_generic_finding(
|
||||
severity=5,
|
||||
title="Weak segmentation - Machines were able to communicate over unused ports.",
|
||||
description="Use micro-segmentation policies to disable communication other than the required.",
|
||||
recommendation="Machines are not locked down at port level. Network tunnel was set up from {0} to {1}"
|
||||
.format(issue['machine'], issue['dest']),
|
||||
instance_arn=instance_arn,
|
||||
instance_id=issue['aws_instance_id'] if 'aws_instance_id' in issue else None
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _handle_sambacry_issue(issue, instance_arn):
|
||||
|
||||
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):
|
||||
|
||||
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):
|
||||
|
||||
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):
|
||||
|
||||
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):
|
||||
|
||||
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):
|
||||
|
||||
return AWSExporter._build_generic_finding(
|
||||
severity=1,
|
||||
title="Weak segmentation - Machines from different segments are able to communicate.",
|
||||
description="Segment your network and make sure there is no communication between machines from different segments.",
|
||||
recommendation="The network can probably be segmented. A monkey instance on \
|
||||
{0} in the networks {1} \
|
||||
could directly access the Monkey Island server in the networks {2}.".format(issue['machine'],
|
||||
issue['networks'],
|
||||
issue['server_networks']),
|
||||
instance_arn=instance_arn,
|
||||
instance_id=issue['aws_instance_id'] if 'aws_instance_id' in issue else None
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _handle_shared_passwords_issue(issue, instance_arn):
|
||||
|
||||
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):
|
||||
|
||||
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):
|
||||
|
||||
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):
|
||||
|
||||
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):
|
||||
|
||||
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):
|
||||
|
||||
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):
|
||||
|
||||
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):
|
||||
|
||||
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):
|
||||
|
||||
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):
|
||||
|
||||
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):
|
||||
|
||||
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):
|
||||
|
||||
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
|
||||
)
|
|
@ -0,0 +1,7 @@
|
|||
class Exporter(object):
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def handle_report(report_json):
|
||||
raise NotImplementedError
|
|
@ -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()
|
|
@ -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)
|
||||
|
|
|
@ -191,6 +191,10 @@ 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']:
|
||||
if 'instance_id' in telemetry_json['data']['aws']:
|
||||
mongo.db.monkey.update_one({'_id': monkey_id},
|
||||
{'$set': {'aws_instance_id': telemetry_json['data']['aws']['instance_id']}})
|
||||
|
||||
@staticmethod
|
||||
def add_ip_to_ssh_keys(ip, ssh_info):
|
||||
|
|
|
@ -27,7 +27,9 @@ ENCRYPTED_CONFIG_ARRAYS = \
|
|||
# This should be used for config values of string type
|
||||
ENCRYPTED_CONFIG_STRINGS = \
|
||||
[
|
||||
|
||||
['cnc', 'aws_config', 'aws_access_key_id'],
|
||||
['cnc', 'aws_config', 'aws_account_id'],
|
||||
['cnc', 'aws_config', 'aws_secret_access_key']
|
||||
]
|
||||
|
||||
|
||||
|
@ -38,11 +40,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 {}
|
||||
|
@ -50,6 +53,8 @@ class ConfigService:
|
|||
config.pop(field, None)
|
||||
if should_decrypt and len(config) > 0:
|
||||
ConfigService.decrypt_config(config)
|
||||
if not is_island:
|
||||
config.get('cnc', {}).pop('aws_config', None)
|
||||
return config
|
||||
|
||||
@staticmethod
|
||||
|
@ -223,11 +228,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
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
WARNING_SIGN = u" \u26A0"
|
||||
|
||||
|
||||
SCHEMA = {
|
||||
"title": "Monkey",
|
||||
"type": "object",
|
||||
|
@ -656,6 +655,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': ''
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -885,4 +909,4 @@ SCHEMA = {
|
|||
"options": {
|
||||
"collapsed": True
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -3,11 +3,14 @@ import functools
|
|||
|
||||
import ipaddress
|
||||
import logging
|
||||
|
||||
from bson import json_util
|
||||
from enum import Enum
|
||||
|
||||
from six import text_type
|
||||
|
||||
from cc.database import mongo
|
||||
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
|
||||
|
@ -125,9 +128,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']
|
||||
})
|
||||
|
||||
|
@ -549,12 +552,24 @@ 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
|
||||
|
||||
@staticmethod
|
||||
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:
|
||||
if 'aws_instance_id' in aws_instance_id_list[0]:
|
||||
return str(aws_instance_id_list[0]['aws_instance_id'])
|
||||
else:
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def get_issues():
|
||||
ISSUE_GENERATORS = [
|
||||
|
@ -572,8 +587,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
|
||||
|
@ -671,26 +689,17 @@ class ReportService:
|
|||
|
||||
@staticmethod
|
||||
def is_report_generated():
|
||||
generated_report = mongo.db.report.find_one({'name': 'generated_report'})
|
||||
if generated_report is None:
|
||||
return False
|
||||
return generated_report['value']
|
||||
generated_report = mongo.db.report.find_one({})
|
||||
return generated_report is not None
|
||||
|
||||
@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 = \
|
||||
{
|
||||
|
@ -722,15 +731,59 @@ 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()
|
||||
ReportExporterManager().export(report)
|
||||
mongo.db.report.drop()
|
||||
mongo.db.report.insert_one(ReportService.encode_dot_char_before_mongo_insert(report))
|
||||
|
||||
return report
|
||||
|
||||
@staticmethod
|
||||
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
|
||||
,,, combo instead.
|
||||
:return: dict with formatted keys with no dots.
|
||||
"""
|
||||
report_as_json = json_util.dumps(report_dict).replace('.', ',,,')
|
||||
return json_util.loads(report_as_json)
|
||||
|
||||
|
||||
@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:
|
||||
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 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 ReportService.decode_dot_char_before_mongo_insert(mongo.db.report.find_one())
|
||||
return ReportService.generate_report()
|
||||
|
||||
@staticmethod
|
||||
def did_exploit_type_succeed(exploit_type):
|
||||
return mongo.db.edge.count(
|
||||
|
|
|
@ -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 = [];
|
||||
|
@ -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'},
|
||||
|
|
|
@ -14,4 +14,6 @@ netifaces
|
|||
ipaddress
|
||||
enum34
|
||||
PyCrypto
|
||||
boto3
|
||||
awscli
|
||||
virtualenv
|
|
@ -14,3 +14,5 @@ netifaces
|
|||
ipaddress
|
||||
enum34
|
||||
PyCrypto
|
||||
boto3
|
||||
awscli
|
Loading…
Reference in New Issue