monkey/monkey/monkey_island/cc/services/reporting/report.py

860 lines
36 KiB
Python

import functools
import ipaddress
import itertools
import logging
from enum import Enum
from bson import json_util
from common.network.network_range import NetworkRange
from common.network.segmentation_utils import get_ip_in_src_and_not_in_dst
from monkey_island.cc.database import mongo
from monkey_island.cc.models import Monkey
from monkey_island.cc.services.utils.network_utils import get_subnets, local_ip_addresses
from monkey_island.cc.services.config import ConfigService
from monkey_island.cc.services.config_schema.config_value_paths import (EXPLOITER_CLASSES_PATH, LOCAL_NETWORK_SCAN_PATH,
PASSWORD_LIST_PATH, SUBNET_SCAN_LIST_PATH,
USER_LIST_PATH)
from monkey_island.cc.services.configuration.utils import get_config_network_segments_as_subnet_groups
from monkey_island.cc.services.node import NodeService
from monkey_island.cc.services.reporting.pth_report import PTHReportService
from monkey_island.cc.services.reporting.report_exporter_manager import ReportExporterManager
from monkey_island.cc.services.reporting.report_generation_synchronisation import safe_generate_regular_report
__author__ = "itay.mizeretz"
logger = logging.getLogger(__name__)
class ReportService:
def __init__(self):
pass
EXPLOIT_DISPLAY_DICT = \
{
'SmbExploiter': 'SMB Exploiter',
'WmiExploiter': 'WMI Exploiter',
'SSHExploiter': 'SSH Exploiter',
'SambaCryExploiter': 'SambaCry Exploiter',
'ElasticGroovyExploiter': 'Elastic Groovy Exploiter',
'Ms08_067_Exploiter': 'Conficker Exploiter',
'ShellShockExploiter': 'ShellShock Exploiter',
'Struts2Exploiter': 'Struts2 Exploiter',
'WebLogicExploiter': 'Oracle WebLogic Exploiter',
'HadoopExploiter': 'Hadoop/Yarn Exploiter',
'MSSQLExploiter': 'MSSQL Exploiter',
'VSFTPDExploiter': 'VSFTPD Backdoor Exploiter',
'DrupalExploiter': 'Drupal Server Exploiter',
'ZerologonExploiter': 'Windows Server Zerologon Exploiter'
}
class ISSUES_DICT(Enum):
WEAK_PASSWORD = 0
STOLEN_CREDS = 1
ELASTIC = 2
SAMBACRY = 3
SHELLSHOCK = 4
CONFICKER = 5
AZURE = 6
STOLEN_SSH_KEYS = 7
STRUTS2 = 8
WEBLOGIC = 9
HADOOP = 10
PTH_CRIT_SERVICES_ACCESS = 11
MSSQL = 12
VSFTPD = 13
DRUPAL = 14
ZEROLOGON = 15
class WARNINGS_DICT(Enum):
CROSS_SEGMENT = 0
TUNNEL = 1
SHARED_LOCAL_ADMIN = 2
SHARED_PASSWORDS = 3
@staticmethod
def get_first_monkey_time():
return mongo.db.telemetry.find({}, {'timestamp': 1}).sort([('$natural', 1)]).limit(1)[0]['timestamp']
@staticmethod
def get_last_monkey_dead_time():
return mongo.db.telemetry.find({}, {'timestamp': 1}).sort([('$natural', -1)]).limit(1)[0]['timestamp']
@staticmethod
def get_monkey_duration():
delta = ReportService.get_last_monkey_dead_time() - ReportService.get_first_monkey_time()
st = ""
hours, rem = divmod(delta.seconds, 60 * 60)
minutes, seconds = divmod(rem, 60)
if delta.days > 0:
st += "%d days, " % delta.days
if hours > 0:
st += "%d hours, " % hours
st += "%d minutes and %d seconds" % (minutes, seconds)
return st
@staticmethod
def get_tunnels():
return [
{
'type': 'tunnel',
'machine': NodeService.get_node_hostname(NodeService.get_node_or_monkey_by_id(tunnel['_id'])),
'dest': NodeService.get_node_hostname(NodeService.get_node_or_monkey_by_id(tunnel['tunnel']))
}
for tunnel in mongo.db.monkey.find({'tunnel': {'$exists': True}}, {'tunnel': 1})]
@staticmethod
def get_azure_issues():
creds = ReportService.get_azure_creds()
machines = set([instance['origin'] for instance in creds])
logger.info('Azure issues generated for reporting')
return [
{
'type': 'azure_password',
'machine': machine,
'users': set([instance['username'] for instance in creds if instance['origin'] == machine])
}
for machine in machines]
@staticmethod
def get_scanned():
formatted_nodes = []
nodes = ReportService.get_all_displayed_nodes()
for node in nodes:
nodes_that_can_access_current_node = node['accessible_from_nodes_hostnames']
formatted_nodes.append(
{
'label': node['label'],
'ip_addresses': node['ip_addresses'],
'accessible_from_nodes': nodes_that_can_access_current_node,
'services': node['services'],
'domain_name': node['domain_name'],
'pba_results': node['pba_results'] if 'pba_results' in node else 'None'
})
logger.info('Scanned nodes generated for reporting')
return formatted_nodes
@staticmethod
def get_all_displayed_nodes():
nodes_without_monkeys = [NodeService.get_displayed_node_by_id(node['_id'], True) for node in
mongo.db.node.find({}, {'_id': 1})]
nodes_with_monkeys = [NodeService.get_displayed_node_by_id(monkey['_id'], True) for monkey in
mongo.db.monkey.find({}, {'_id': 1})]
nodes = nodes_without_monkeys + nodes_with_monkeys
return nodes
@staticmethod
def get_exploited():
exploited_with_monkeys = \
[NodeService.get_displayed_node_by_id(monkey['_id'], True) for monkey in
mongo.db.monkey.find({}, {'_id': 1}) if
not NodeService.get_monkey_manual_run(NodeService.get_monkey_by_id(monkey['_id']))]
exploited_without_monkeys = [NodeService.get_displayed_node_by_id(node['_id'], True) for node in
mongo.db.node.find({'exploited': True}, {'_id': 1})]
exploited = exploited_with_monkeys + exploited_without_monkeys
exploited = [
{
'label': exploited_node['label'],
'ip_addresses': exploited_node['ip_addresses'],
'domain_name': exploited_node['domain_name'],
'exploits': list(set(
[ReportService.EXPLOIT_DISPLAY_DICT[exploit['exploiter']] for exploit in exploited_node['exploits']
if exploit['result']]))
}
for exploited_node in exploited]
logger.info('Exploited nodes generated for reporting')
return exploited
@staticmethod
def get_stolen_creds():
creds = []
stolen_system_info_creds = ReportService._get_credentials_from_system_info_telems()
creds.extend(stolen_system_info_creds)
stolen_exploit_creds = ReportService._get_credentials_from_exploit_telems()
creds.extend(stolen_exploit_creds)
logger.info('Stolen creds generated for reporting')
return creds
@staticmethod
def _get_credentials_from_system_info_telems():
formatted_creds = []
for telem in mongo.db.telemetry.find({'telem_category': 'system_info', 'data.credentials': {'$exists': True}},
{'data.credentials': 1, 'monkey_guid': 1}):
creds = telem['data']['credentials']
formatted_creds.extend(ReportService._format_creds_for_reporting(telem, creds))
return formatted_creds
@staticmethod
def _get_credentials_from_exploit_telems():
formatted_creds = []
for telem in mongo.db.telemetry.find({'telem_category': 'exploit', 'data.info.credentials': {'$exists': True}},
{'data.info.credentials': 1, 'monkey_guid': 1}):
creds = telem['data']['info']['credentials']
formatted_creds.extend(ReportService._format_creds_for_reporting(telem, creds))
return formatted_creds
@staticmethod
def _format_creds_for_reporting(telem, monkey_creds):
creds = []
CRED_TYPE_DICT = {'password': 'Clear Password', 'lm_hash': 'LM hash', 'ntlm_hash': 'NTLM hash'}
if len(monkey_creds) == 0:
return []
origin = NodeService.get_monkey_by_guid(telem['monkey_guid'])['hostname']
for user in monkey_creds:
for cred_type in CRED_TYPE_DICT:
if cred_type not in monkey_creds[user] or not monkey_creds[user][cred_type]:
continue
username = monkey_creds[user]['username'] if 'username' in monkey_creds[user] else user
cred_row = \
{
'username': username,
'type': CRED_TYPE_DICT[cred_type],
'origin': origin
}
if cred_row not in creds:
creds.append(cred_row)
return creds
@staticmethod
def get_ssh_keys():
"""
Return private ssh keys found as credentials
:return: List of credentials
"""
creds = []
for telem in mongo.db.telemetry.find(
{'telem_category': 'system_info', 'data.ssh_info': {'$exists': True}},
{'data.ssh_info': 1, 'monkey_guid': 1}
):
origin = NodeService.get_monkey_by_guid(telem['monkey_guid'])['hostname']
if telem['data']['ssh_info']:
# Pick out all ssh keys not yet included in creds
ssh_keys = [{'username': key_pair['name'], 'type': 'Clear SSH private key',
'origin': origin} for key_pair in telem['data']['ssh_info']
if
key_pair['private_key'] and {'username': key_pair['name'], 'type': 'Clear SSH private key',
'origin': origin} not in creds]
creds.extend(ssh_keys)
return creds
@staticmethod
def get_azure_creds():
"""
Recover all credentials marked as being from an Azure machine
:return: List of credentials.
"""
creds = []
for telem in mongo.db.telemetry.find(
{'telem_category': 'system_info', 'data.Azure': {'$exists': True}},
{'data.Azure': 1, 'monkey_guid': 1}
):
azure_users = telem['data']['Azure']['usernames']
if len(azure_users) == 0:
continue
origin = NodeService.get_monkey_by_guid(telem['monkey_guid'])['hostname']
azure_leaked_users = [{'username': user.replace(',', '.'), 'type': 'Clear Password',
'origin': origin} for user in azure_users]
creds.extend(azure_leaked_users)
logger.info('Azure machines creds generated for reporting')
return creds
@staticmethod
def process_general_exploit(exploit):
ip_addr = exploit['data']['machine']['ip_addr']
return {'machine': NodeService.get_node_hostname(NodeService.get_node_or_monkey_by_ip(ip_addr)),
'ip_address': ip_addr}
@staticmethod
def process_general_creds_exploit(exploit):
processed_exploit = ReportService.process_general_exploit(exploit)
for attempt in exploit['data']['attempts']:
if attempt['result']:
processed_exploit['username'] = attempt['user']
if attempt['password']:
processed_exploit['type'] = 'password'
processed_exploit['password'] = attempt['password']
elif attempt['ssh_key']:
processed_exploit['type'] = 'ssh_key'
processed_exploit['ssh_key'] = attempt['ssh_key']
else:
processed_exploit['type'] = 'hash'
return processed_exploit
return processed_exploit
@staticmethod
def process_smb_exploit(exploit):
processed_exploit = ReportService.process_general_creds_exploit(exploit)
if processed_exploit['type'] == 'password':
processed_exploit['type'] = 'smb_password'
else:
processed_exploit['type'] = 'smb_pth'
return processed_exploit
@staticmethod
def process_wmi_exploit(exploit):
processed_exploit = ReportService.process_general_creds_exploit(exploit)
if processed_exploit['type'] == 'password':
processed_exploit['type'] = 'wmi_password'
else:
processed_exploit['type'] = 'wmi_pth'
return processed_exploit
@staticmethod
def process_ssh_exploit(exploit):
processed_exploit = ReportService.process_general_creds_exploit(exploit)
# Check if it's ssh key or ssh login credentials exploit
if processed_exploit['type'] == 'ssh_key':
return processed_exploit
else:
processed_exploit['type'] = 'ssh'
return processed_exploit
@staticmethod
def process_vsftpd_exploit(exploit):
processed_exploit = ReportService.process_general_creds_exploit(exploit)
processed_exploit['type'] = 'vsftp'
return processed_exploit
@staticmethod
def process_sambacry_exploit(exploit):
processed_exploit = ReportService.process_general_creds_exploit(exploit)
processed_exploit['type'] = 'sambacry'
return processed_exploit
@staticmethod
def process_elastic_exploit(exploit):
processed_exploit = ReportService.process_general_exploit(exploit)
processed_exploit['type'] = 'elastic'
return processed_exploit
@staticmethod
def process_conficker_exploit(exploit):
processed_exploit = ReportService.process_general_exploit(exploit)
processed_exploit['type'] = 'conficker'
return processed_exploit
@staticmethod
def process_shellshock_exploit(exploit):
processed_exploit = ReportService.process_general_exploit(exploit)
processed_exploit['type'] = 'shellshock'
urls = exploit['data']['info']['vulnerable_urls']
processed_exploit['port'] = urls[0].split(':')[2].split('/')[0]
processed_exploit['paths'] = ['/' + url.split(':')[2].split('/')[1] for url in urls]
return processed_exploit
@staticmethod
def process_struts2_exploit(exploit):
processed_exploit = ReportService.process_general_exploit(exploit)
processed_exploit['type'] = 'struts2'
return processed_exploit
@staticmethod
def process_weblogic_exploit(exploit):
processed_exploit = ReportService.process_general_exploit(exploit)
processed_exploit['type'] = 'weblogic'
return processed_exploit
@staticmethod
def process_hadoop_exploit(exploit):
processed_exploit = ReportService.process_general_exploit(exploit)
processed_exploit['type'] = 'hadoop'
return processed_exploit
@staticmethod
def process_mssql_exploit(exploit):
processed_exploit = ReportService.process_general_exploit(exploit)
processed_exploit['type'] = 'mssql'
return processed_exploit
@staticmethod
def process_drupal_exploit(exploit):
processed_exploit = ReportService.process_general_exploit(exploit)
processed_exploit['type'] = 'drupal'
return processed_exploit
@staticmethod
def process_zerologon_exploit(exploit):
processed_exploit = ReportService.process_general_exploit(exploit)
processed_exploit['type'] = 'zerologon'
processed_exploit['password_restore_success'] = exploit['data']['info']['password_restore_success']
return processed_exploit
@staticmethod
def process_exploit(exploit):
exploiter_type = exploit['data']['exploiter']
EXPLOIT_PROCESS_FUNCTION_DICT = {
'SmbExploiter': ReportService.process_smb_exploit,
'WmiExploiter': ReportService.process_wmi_exploit,
'SSHExploiter': ReportService.process_ssh_exploit,
'SambaCryExploiter': ReportService.process_sambacry_exploit,
'ElasticGroovyExploiter': ReportService.process_elastic_exploit,
'Ms08_067_Exploiter': ReportService.process_conficker_exploit,
'ShellShockExploiter': ReportService.process_shellshock_exploit,
'Struts2Exploiter': ReportService.process_struts2_exploit,
'WebLogicExploiter': ReportService.process_weblogic_exploit,
'HadoopExploiter': ReportService.process_hadoop_exploit,
'MSSQLExploiter': ReportService.process_mssql_exploit,
'VSFTPDExploiter': ReportService.process_vsftpd_exploit,
'DrupalExploiter': ReportService.process_drupal_exploit,
'ZerologonExploiter': ReportService.process_zerologon_exploit
}
return EXPLOIT_PROCESS_FUNCTION_DICT[exploiter_type](exploit)
@staticmethod
def get_exploits():
query = [{'$match': {'telem_category': 'exploit', 'data.result': True}},
{'$group': {'_id': {'ip_address': '$data.machine.ip_addr'},
'data': {'$first': '$$ROOT'},
}},
{"$replaceRoot": {"newRoot": "$data"}}]
exploits = []
for exploit in mongo.db.telemetry.aggregate(query):
new_exploit = ReportService.process_exploit(exploit)
if new_exploit not in exploits:
exploits.append(new_exploit)
return exploits
@staticmethod
def get_monkey_subnets(monkey_guid):
network_info = mongo.db.telemetry.find_one(
{'telem_category': 'system_info',
'monkey_guid': monkey_guid},
{'data.network_info.networks': 1}
)
if network_info is None or not network_info["data"]:
return []
return \
[
ipaddress.ip_interface(str(network['addr'] + '/' + network['netmask'])).network
for network in network_info['data']['network_info']['networks']
]
@staticmethod
def get_island_cross_segment_issues():
issues = []
island_ips = local_ip_addresses()
for monkey in mongo.db.monkey.find({'tunnel': {'$exists': False}}, {'tunnel': 1, 'guid': 1, 'hostname': 1}):
found_good_ip = False
monkey_subnets = ReportService.get_monkey_subnets(monkey['guid'])
for subnet in monkey_subnets:
for ip in island_ips:
if ipaddress.ip_address(str(ip)) in subnet:
found_good_ip = True
break
if found_good_ip:
break
if not found_good_ip:
issues.append(
{'type': 'island_cross_segment', 'machine': monkey['hostname'],
'networks': [str(subnet) for subnet in monkey_subnets],
'server_networks': [str(subnet) for subnet in get_subnets()]}
)
return issues
@staticmethod
def get_cross_segment_issues_of_single_machine(source_subnet_range, target_subnet_range):
"""
Gets list of cross segment issues of a single machine. Meaning a machine has an interface for each of the
subnets.
:param source_subnet_range: The subnet range which shouldn't be able to access target_subnet.
:param target_subnet_range: The subnet range which shouldn't be accessible from source_subnet.
:return:
"""
cross_segment_issues = []
for monkey in mongo.db.monkey.find({}, {'ip_addresses': 1, 'hostname': 1}):
ip_in_src = None
ip_in_dst = None
for ip_addr in monkey['ip_addresses']:
if source_subnet_range.is_in_range(str(ip_addr)):
ip_in_src = ip_addr
break
# No point searching the dst subnet if there are no IPs in src subnet.
if not ip_in_src:
continue
for ip_addr in monkey['ip_addresses']:
if target_subnet_range.is_in_range(str(ip_addr)):
ip_in_dst = ip_addr
break
if ip_in_dst:
cross_segment_issues.append(
{
'source': ip_in_src,
'hostname': monkey['hostname'],
'target': ip_in_dst,
'services': None,
'is_self': True
})
return cross_segment_issues
@staticmethod
def get_cross_segment_issues_per_subnet_pair(scans, source_subnet, target_subnet):
"""
Gets list of cross segment issues from source_subnet to target_subnet.
:param scans: List of all scan telemetry entries. Must have monkey_guid, ip_addr and services.
This should be a PyMongo cursor object.
:param source_subnet: The subnet which shouldn't be able to access target_subnet.
:param target_subnet: The subnet which shouldn't be accessible from source_subnet.
:return:
"""
if source_subnet == target_subnet:
return []
source_subnet_range = NetworkRange.get_range_obj(source_subnet)
target_subnet_range = NetworkRange.get_range_obj(target_subnet)
cross_segment_issues = []
scans.rewind() # If we iterated over scans already we need to rewind.
for scan in scans:
target_ip = scan['data']['machine']['ip_addr']
if target_subnet_range.is_in_range(str(target_ip)):
monkey = NodeService.get_monkey_by_guid(scan['monkey_guid'])
cross_segment_ip = get_ip_in_src_and_not_in_dst(monkey['ip_addresses'],
source_subnet_range,
target_subnet_range)
if cross_segment_ip is not None:
cross_segment_issues.append(
{
'source': cross_segment_ip,
'hostname': monkey['hostname'],
'target': target_ip,
'services': scan['data']['machine']['services'],
'icmp': scan['data']['machine']['icmp'],
'is_self': False
})
return cross_segment_issues + ReportService.get_cross_segment_issues_of_single_machine(
source_subnet_range, target_subnet_range)
@staticmethod
def get_cross_segment_issues_per_subnet_group(scans, subnet_group):
"""
Gets list of cross segment issues within given subnet_group.
:param scans: List of all scan telemetry entries. Must have monkey_guid, ip_addr and services.
This should be a PyMongo cursor object.
:param subnet_group: List of subnets which shouldn't be accessible from each other.
:return: Cross segment issues regarding the subnets in the group.
"""
cross_segment_issues = []
for subnet_pair in itertools.product(subnet_group, subnet_group):
source_subnet = subnet_pair[0]
target_subnet = subnet_pair[1]
pair_issues = ReportService.get_cross_segment_issues_per_subnet_pair(scans, source_subnet, target_subnet)
if len(pair_issues) != 0:
cross_segment_issues.append(
{
'source_subnet': source_subnet,
'target_subnet': target_subnet,
'issues': pair_issues
})
return cross_segment_issues
@staticmethod
def get_cross_segment_issues():
scans = mongo.db.telemetry.find({'telem_category': 'scan'},
{'monkey_guid': 1, 'data.machine.ip_addr': 1, 'data.machine.services': 1, 'data.machine.icmp': 1})
cross_segment_issues = []
# For now the feature is limited to 1 group.
subnet_groups = get_config_network_segments_as_subnet_groups()
for subnet_group in subnet_groups:
cross_segment_issues += ReportService.get_cross_segment_issues_per_subnet_group(scans, subnet_group)
return cross_segment_issues
@staticmethod
def get_domain_issues():
ISSUE_GENERATORS = [
PTHReportService.get_duplicated_passwords_issues,
PTHReportService.get_shared_admins_issues,
]
issues = functools.reduce(lambda acc, issue_gen: acc + issue_gen(), ISSUE_GENERATORS, [])
domain_issues_dict = {}
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 = [
ReportService.get_exploits,
ReportService.get_tunnels,
ReportService.get_island_cross_segment_issues,
ReportService.get_azure_issues,
PTHReportService.get_duplicated_passwords_issues,
PTHReportService.get_strong_users_on_crit_issues
]
issues = functools.reduce(lambda acc, issue_gen: acc + issue_gen(), ISSUE_GENERATORS, [])
issues_dict = {}
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
@staticmethod
def get_manual_monkeys():
return [monkey['hostname'] for monkey in mongo.db.monkey.find({}, {'hostname': 1, 'parent': 1, 'guid': 1}) if
NodeService.get_monkey_manual_run(monkey)]
@staticmethod
def get_config_users():
return ConfigService.get_config_value(USER_LIST_PATH, True, True)
@staticmethod
def get_config_passwords():
return ConfigService.get_config_value(PASSWORD_LIST_PATH, True, True)
@staticmethod
def get_config_exploits():
exploits_config_value = EXPLOITER_CLASSES_PATH
default_exploits = ConfigService.get_default_config(False)
for namespace in exploits_config_value:
default_exploits = default_exploits[namespace]
exploits = ConfigService.get_config_value(exploits_config_value, True, True)
if exploits == default_exploits:
return ['default']
return [ReportService.EXPLOIT_DISPLAY_DICT[exploit] for exploit in
exploits]
@staticmethod
def get_config_ips():
return ConfigService.get_config_value(SUBNET_SCAN_LIST_PATH, True, True)
@staticmethod
def get_config_scan():
return ConfigService.get_config_value(LOCAL_NETWORK_SCAN_PATH, True, True)
@staticmethod
def get_issues_overview(issues, config_users, config_passwords):
issues_byte_array = [False] * len(ReportService.ISSUES_DICT)
for machine in issues:
for issue in issues[machine]:
if issue['type'] == 'elastic':
issues_byte_array[ReportService.ISSUES_DICT.ELASTIC.value] = True
elif issue['type'] == 'sambacry':
issues_byte_array[ReportService.ISSUES_DICT.SAMBACRY.value] = True
elif issue['type'] == 'vsftp':
issues_byte_array[ReportService.ISSUES_DICT.VSFTPD.value] = True
elif issue['type'] == 'shellshock':
issues_byte_array[ReportService.ISSUES_DICT.SHELLSHOCK.value] = True
elif issue['type'] == 'conficker':
issues_byte_array[ReportService.ISSUES_DICT.CONFICKER.value] = True
elif issue['type'] == 'azure_password':
issues_byte_array[ReportService.ISSUES_DICT.AZURE.value] = True
elif issue['type'] == 'ssh_key':
issues_byte_array[ReportService.ISSUES_DICT.STOLEN_SSH_KEYS.value] = True
elif issue['type'] == 'struts2':
issues_byte_array[ReportService.ISSUES_DICT.STRUTS2.value] = True
elif issue['type'] == 'weblogic':
issues_byte_array[ReportService.ISSUES_DICT.WEBLOGIC.value] = True
elif issue['type'] == 'mssql':
issues_byte_array[ReportService.ISSUES_DICT.MSSQL.value] = True
elif issue['type'] == 'hadoop':
issues_byte_array[ReportService.ISSUES_DICT.HADOOP.value] = True
elif issue['type'] == 'drupal':
issues_byte_array[ReportService.ISSUES_DICT.DRUPAL.value] = True
elif issue['type'] == 'zerologon':
issues_byte_array[ReportService.ISSUES_DICT.ZEROLOGON.value] = True
elif issue['type'].endswith('_password') and issue['password'] in config_passwords and \
issue['username'] in config_users or issue['type'] == 'ssh':
issues_byte_array[ReportService.ISSUES_DICT.WEAK_PASSWORD.value] = True
elif issue['type'] == 'strong_users_on_crit':
issues_byte_array[ReportService.ISSUES_DICT.PTH_CRIT_SERVICES_ACCESS.value] = True
elif issue['type'].endswith('_pth') or issue['type'].endswith('_password'):
issues_byte_array[ReportService.ISSUES_DICT.STOLEN_CREDS.value] = True
return issues_byte_array
@staticmethod
def get_warnings_overview(issues, cross_segment_issues):
warnings_byte_array = [False] * len(ReportService.WARNINGS_DICT)
for machine in issues:
for issue in issues[machine]:
if issue['type'] == 'island_cross_segment':
warnings_byte_array[ReportService.WARNINGS_DICT.CROSS_SEGMENT.value] = True
elif issue['type'] == 'tunnel':
warnings_byte_array[ReportService.WARNINGS_DICT.TUNNEL.value] = True
elif issue['type'] == 'shared_admins':
warnings_byte_array[ReportService.WARNINGS_DICT.SHARED_LOCAL_ADMIN.value] = True
elif issue['type'] == 'shared_passwords':
warnings_byte_array[ReportService.WARNINGS_DICT.SHARED_PASSWORDS.value] = True
if len(cross_segment_issues) != 0:
warnings_byte_array[ReportService.WARNINGS_DICT.CROSS_SEGMENT.value] = True
return warnings_byte_array
@staticmethod
def is_report_generated():
generated_report = mongo.db.report.find_one({})
return generated_report is not None
@staticmethod
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 = Monkey.get_latest_modifytime()
scanned_nodes = ReportService.get_scanned()
exploited_nodes = ReportService.get_exploited()
report = \
{
'overview':
{
'manual_monkeys': ReportService.get_manual_monkeys(),
'config_users': config_users,
'config_passwords': config_passwords,
'config_exploits': ReportService.get_config_exploits(),
'config_ips': ReportService.get_config_ips(),
'config_scan': ReportService.get_config_scan(),
'monkey_start_time': ReportService.get_first_monkey_time().strftime("%d/%m/%Y %H:%M:%S"),
'monkey_duration': ReportService.get_monkey_duration(),
'issues': ReportService.get_issues_overview(issues, config_users, config_passwords),
'warnings': ReportService.get_warnings_overview(issues, cross_segment_issues),
'cross_segment_issues': cross_segment_issues
},
'glance':
{
'scanned': scanned_nodes,
'exploited': exploited_nodes,
'stolen_creds': ReportService.get_stolen_creds(),
'azure_passwords': ReportService.get_azure_creds(),
'ssh_keys': ReportService.get_ssh_keys(),
'strong_users': PTHReportService.get_strong_users_on_crit_details()
},
'recommendations':
{
'issues': issues,
'domain_issues': domain_issues
},
'meta':
{
'latest_monkey_modifytime': monkey_latest_modify_time
}
}
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 = Monkey.get_latest_modifytime()
return report_latest_modifytime == latest_monkey_modifytime
return False
@staticmethod
def delete_saved_report_if_exists():
"""
This function clears the saved report from the DB.
:raises RuntimeError if deletion failed
"""
delete_result = mongo.db.report.delete_many({})
if mongo.db.report.count_documents({}) != 0:
raise RuntimeError("Report cache not cleared. DeleteResult: " + delete_result.raw_result)
@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 safe_generate_regular_report()
@staticmethod
def did_exploit_type_succeed(exploit_type):
return mongo.db.edge.count(
{'exploits': {'$elemMatch': {'exploiter': exploit_type, 'result': True}}},
limit=1) > 0