diff --git a/deployment_scripts/config.ps1 b/deployment_scripts/config.ps1 index 24a8d3322..07be64612 100644 --- a/deployment_scripts/config.ps1 +++ b/deployment_scripts/config.ps1 @@ -22,7 +22,7 @@ $SAMBA_64_BINARY_NAME = "sc_monkey_runner64.so" # Other directories and paths ( most likely you dont need to configure) $MONKEY_ISLAND_DIR = "\monkey\monkey_island" $MONKEY_DIR = "\monkey\infection_monkey" -$SAMBA_BINARIES_DIR = Join-Path -Path $MONKEY_DIR -ChildPath "\monkey_utils\sambacry_monkey_runner" +$SAMBA_BINARIES_DIR = Join-Path -Path $MONKEY_DIR -ChildPath "\exploit\sambacry_monkey_runner" $PYTHON_DLL = "C:\Windows\System32\python27.dll" $MK32_DLL = "mk32.dll" $MK64_DLL = "mk64.dll" diff --git a/deployment_scripts/deploy_linux.sh b/deployment_scripts/deploy_linux.sh index 5ce29ac59..4df8ba114 100644 --- a/deployment_scripts/deploy_linux.sh +++ b/deployment_scripts/deploy_linux.sh @@ -129,7 +129,7 @@ python -m pip install --user -r requirements_linux.txt || handle_error # Build samba log_message "Building samba binaries" sudo apt-get install gcc-multilib -cd ${monkey_home}/monkey/infection_monkey/monkey_utils/sambacry_monkey_runner +cd ${monkey_home}/monkey/infection_monkey/exploit/sambacry_monkey_runner sudo chmod +x ./build.sh || handle_error ./build.sh diff --git a/monkey/common/data/__init__.py b/monkey/common/data/__init__.py new file mode 100644 index 000000000..a8c1a93f7 --- /dev/null +++ b/monkey/common/data/__init__.py @@ -0,0 +1,2 @@ +from zero_trust_consts import populate_mappings +populate_mappings() diff --git a/monkey/common/data/network_consts.py b/monkey/common/data/network_consts.py new file mode 100644 index 000000000..5fc9d6d8a --- /dev/null +++ b/monkey/common/data/network_consts.py @@ -0,0 +1,2 @@ +ES_SERVICE = 'elastic-search-9200' + diff --git a/monkey/common/data/post_breach_consts.py b/monkey/common/data/post_breach_consts.py new file mode 100644 index 000000000..dee4f67d0 --- /dev/null +++ b/monkey/common/data/post_breach_consts.py @@ -0,0 +1,3 @@ +POST_BREACH_COMMUNICATE_AS_NEW_USER = "Communicate as new user" +POST_BREACH_BACKDOOR_USER = "Backdoor user" +POST_BREACH_FILE_EXECUTION = "File execution" diff --git a/monkey/common/data/zero_trust_consts.py b/monkey/common/data/zero_trust_consts.py new file mode 100644 index 000000000..4add05d04 --- /dev/null +++ b/monkey/common/data/zero_trust_consts.py @@ -0,0 +1,205 @@ +""" +This file contains all the static data relating to Zero Trust. It is mostly used in the zero trust report generation and +in creating findings. + +This file contains static mappings between zero trust components such as: pillars, principles, tests, statuses. +Some of the mappings are computed when this module is loaded. +""" + +AUTOMATION_ORCHESTRATION = u"Automation & Orchestration" +VISIBILITY_ANALYTICS = u"Visibility & Analytics" +WORKLOADS = u"Workloads" +DEVICES = u"Devices" +NETWORKS = u"Networks" +PEOPLE = u"People" +DATA = u"Data" +PILLARS = (DATA, PEOPLE, NETWORKS, DEVICES, WORKLOADS, VISIBILITY_ANALYTICS, AUTOMATION_ORCHESTRATION) + +STATUS_UNEXECUTED = u"Unexecuted" +STATUS_PASSED = u"Passed" +STATUS_VERIFY = u"Verify" +STATUS_FAILED = u"Failed" +# Don't change order! The statuses are ordered by importance/severity. +ORDERED_TEST_STATUSES = [STATUS_FAILED, STATUS_VERIFY, STATUS_PASSED, STATUS_UNEXECUTED] + +TEST_DATA_ENDPOINT_ELASTIC = u"unencrypted_data_endpoint_elastic" +TEST_DATA_ENDPOINT_HTTP = u"unencrypted_data_endpoint_http" +TEST_MACHINE_EXPLOITED = u"machine_exploited" +TEST_ENDPOINT_SECURITY_EXISTS = u"endpoint_security_exists" +TEST_SCHEDULED_EXECUTION = u"scheduled_execution" +TEST_MALICIOUS_ACTIVITY_TIMELINE = u"malicious_activity_timeline" +TEST_SEGMENTATION = u"segmentation" +TEST_TUNNELING = u"tunneling" +TEST_COMMUNICATE_AS_NEW_USER = u"communicate_as_new_user" +TESTS = ( + TEST_SEGMENTATION, + TEST_MALICIOUS_ACTIVITY_TIMELINE, + TEST_SCHEDULED_EXECUTION, + TEST_ENDPOINT_SECURITY_EXISTS, + TEST_MACHINE_EXPLOITED, + TEST_DATA_ENDPOINT_HTTP, + TEST_DATA_ENDPOINT_ELASTIC, + TEST_TUNNELING, + TEST_COMMUNICATE_AS_NEW_USER +) + +PRINCIPLE_DATA_TRANSIT = u"data_transit" +PRINCIPLE_ENDPOINT_SECURITY = u"endpoint_security" +PRINCIPLE_USER_BEHAVIOUR = u"user_behaviour" +PRINCIPLE_ANALYZE_NETWORK_TRAFFIC = u"analyze_network_traffic" +PRINCIPLE_SEGMENTATION = u"segmentation" +PRINCIPLE_RESTRICTIVE_NETWORK_POLICIES = u"network_policies" +PRINCIPLE_USERS_MAC_POLICIES = u"users_mac_policies" +PRINCIPLES = { + PRINCIPLE_SEGMENTATION: u"Apply segmentation and micro-segmentation inside your network.", + PRINCIPLE_ANALYZE_NETWORK_TRAFFIC: u"Analyze network traffic for malicious activity.", + PRINCIPLE_USER_BEHAVIOUR: u"Adopt security user behavior analytics.", + PRINCIPLE_ENDPOINT_SECURITY: u"Use anti-virus and other traditional endpoint security solutions.", + PRINCIPLE_DATA_TRANSIT: u"Secure data at transit by encrypting it.", + PRINCIPLE_RESTRICTIVE_NETWORK_POLICIES: u"Configure network policies to be as restrictive as possible.", + PRINCIPLE_USERS_MAC_POLICIES: u"Users' permissions to the network and to resources should be MAC (Mandetory " + u"Access Control) only.", +} + +POSSIBLE_STATUSES_KEY = u"possible_statuses" +PILLARS_KEY = u"pillars" +PRINCIPLE_KEY = u"principle_key" +FINDING_EXPLANATION_BY_STATUS_KEY = u"finding_explanation" +TEST_EXPLANATION_KEY = u"explanation" +TESTS_MAP = { + TEST_SEGMENTATION: { + TEST_EXPLANATION_KEY: u"The Monkey tried to scan and find machines that it can communicate with from the machine it's running on, that belong to different network segments.", + FINDING_EXPLANATION_BY_STATUS_KEY: { + STATUS_FAILED: "Monkey performed cross-segment communication. Check firewall rules and logs.", + STATUS_PASSED: "Monkey couldn't perform cross-segment communication. If relevant, check firewall logs." + }, + PRINCIPLE_KEY: PRINCIPLE_SEGMENTATION, + PILLARS_KEY: [NETWORKS], + POSSIBLE_STATUSES_KEY: [STATUS_UNEXECUTED, STATUS_PASSED, STATUS_FAILED] + }, + TEST_MALICIOUS_ACTIVITY_TIMELINE: { + TEST_EXPLANATION_KEY: u"The Monkeys in the network performed malicious-looking actions, like scanning and attempting exploitation.", + FINDING_EXPLANATION_BY_STATUS_KEY: { + STATUS_VERIFY: "Monkey performed malicious actions in the network. Check SOC logs and alerts." + }, + PRINCIPLE_KEY: PRINCIPLE_ANALYZE_NETWORK_TRAFFIC, + PILLARS_KEY: [NETWORKS, VISIBILITY_ANALYTICS], + POSSIBLE_STATUSES_KEY: [STATUS_UNEXECUTED, STATUS_VERIFY] + }, + TEST_ENDPOINT_SECURITY_EXISTS: { + TEST_EXPLANATION_KEY: u"The Monkey checked if there is an active process of an endpoint security software.", + FINDING_EXPLANATION_BY_STATUS_KEY: { + STATUS_FAILED: "Monkey didn't find ANY active endpoint security processes. Install and activate anti-virus software on endpoints.", + STATUS_PASSED: "Monkey found active endpoint security processes. Check their logs to see if Monkey was a security concern." + }, + PRINCIPLE_KEY: PRINCIPLE_ENDPOINT_SECURITY, + PILLARS_KEY: [DEVICES], + POSSIBLE_STATUSES_KEY: [STATUS_UNEXECUTED, STATUS_FAILED, STATUS_PASSED] + }, + TEST_MACHINE_EXPLOITED: { + TEST_EXPLANATION_KEY: u"The Monkey tries to exploit machines in order to breach them and propagate in the network.", + FINDING_EXPLANATION_BY_STATUS_KEY: { + STATUS_FAILED: "Monkey successfully exploited endpoints. Check IDS/IPS logs to see activity recognized and see which endpoints were compromised.", + STATUS_PASSED: "Monkey didn't manage to exploit an endpoint." + }, + PRINCIPLE_KEY: PRINCIPLE_ENDPOINT_SECURITY, + PILLARS_KEY: [DEVICES], + POSSIBLE_STATUSES_KEY: [STATUS_UNEXECUTED, STATUS_FAILED, STATUS_VERIFY] + }, + TEST_SCHEDULED_EXECUTION: { + TEST_EXPLANATION_KEY: "The Monkey was executed in a scheduled manner.", + FINDING_EXPLANATION_BY_STATUS_KEY: { + STATUS_VERIFY: "Monkey was executed in a scheduled manner. Locate this activity in User-Behavior security software.", + STATUS_PASSED: "Monkey failed to execute in a scheduled manner." + }, + PRINCIPLE_KEY: PRINCIPLE_USER_BEHAVIOUR, + PILLARS_KEY: [PEOPLE, NETWORKS], + POSSIBLE_STATUSES_KEY: [STATUS_UNEXECUTED, STATUS_VERIFY] + }, + TEST_DATA_ENDPOINT_ELASTIC: { + TEST_EXPLANATION_KEY: u"The Monkey scanned for unencrypted access to ElasticSearch instances.", + FINDING_EXPLANATION_BY_STATUS_KEY: { + STATUS_FAILED: "Monkey accessed ElasticSearch instances. Limit access to data by encrypting it in in-transit.", + STATUS_PASSED: "Monkey didn't find open ElasticSearch instances. If you have such instances, look for alerts that indicate attempts to access them." + }, + PRINCIPLE_KEY: PRINCIPLE_DATA_TRANSIT, + PILLARS_KEY: [DATA], + POSSIBLE_STATUSES_KEY: [STATUS_UNEXECUTED, STATUS_FAILED, STATUS_PASSED] + }, + TEST_DATA_ENDPOINT_HTTP: { + TEST_EXPLANATION_KEY: u"The Monkey scanned for unencrypted access to HTTP servers.", + FINDING_EXPLANATION_BY_STATUS_KEY: { + STATUS_FAILED: "Monkey accessed HTTP servers. Limit access to data by encrypting it in in-transit.", + STATUS_PASSED: "Monkey didn't find open HTTP servers. If you have such servers, look for alerts that indicate attempts to access them." + }, + PRINCIPLE_KEY: PRINCIPLE_DATA_TRANSIT, + PILLARS_KEY: [DATA], + POSSIBLE_STATUSES_KEY: [STATUS_UNEXECUTED, STATUS_FAILED, STATUS_PASSED] + }, + TEST_TUNNELING: { + TEST_EXPLANATION_KEY: u"The Monkey tried to tunnel traffic using other monkeys.", + FINDING_EXPLANATION_BY_STATUS_KEY: { + STATUS_FAILED: "Monkey tunneled its traffic using other monkeys. Your network policies are too permissive - restrict them." + }, + PRINCIPLE_KEY: PRINCIPLE_RESTRICTIVE_NETWORK_POLICIES, + PILLARS_KEY: [NETWORKS, VISIBILITY_ANALYTICS], + POSSIBLE_STATUSES_KEY: [STATUS_UNEXECUTED, STATUS_FAILED] + }, + TEST_COMMUNICATE_AS_NEW_USER: { + TEST_EXPLANATION_KEY: u"The Monkey tried to create a new user and communicate with the internet from it.", + FINDING_EXPLANATION_BY_STATUS_KEY: { + STATUS_FAILED: "Monkey caused a new user to access the network. Your network policies are too permissive - restrict them to MAC only.", + STATUS_PASSED: "Monkey wasn't able to cause a new user to access the network." + }, + PRINCIPLE_KEY: PRINCIPLE_USERS_MAC_POLICIES, + PILLARS_KEY: [PEOPLE, NETWORKS, VISIBILITY_ANALYTICS], + POSSIBLE_STATUSES_KEY: [STATUS_UNEXECUTED, STATUS_FAILED, STATUS_PASSED] + }, +} + +EVENT_TYPE_MONKEY_NETWORK = "monkey_network" +EVENT_TYPE_MONKEY_LOCAL = "monkey_local" +EVENT_TYPES = (EVENT_TYPE_MONKEY_LOCAL, EVENT_TYPE_MONKEY_NETWORK) + +PILLARS_TO_TESTS = { + DATA: [], + PEOPLE: [], + NETWORKS: [], + DEVICES: [], + WORKLOADS: [], + VISIBILITY_ANALYTICS: [], + AUTOMATION_ORCHESTRATION: [] +} + +PRINCIPLES_TO_TESTS = {} + +PRINCIPLES_TO_PILLARS = {} + + +def populate_mappings(): + populate_pillars_to_tests() + populate_principles_to_tests() + populate_principles_to_pillars() + + +def populate_pillars_to_tests(): + for pillar in PILLARS: + for test, test_info in TESTS_MAP.items(): + if pillar in test_info[PILLARS_KEY]: + PILLARS_TO_TESTS[pillar].append(test) + + +def populate_principles_to_tests(): + for single_principle in PRINCIPLES: + PRINCIPLES_TO_TESTS[single_principle] = [] + for test, test_info in TESTS_MAP.items(): + PRINCIPLES_TO_TESTS[test_info[PRINCIPLE_KEY]].append(test) + + +def populate_principles_to_pillars(): + for principle, principle_tests in PRINCIPLES_TO_TESTS.items(): + principles_pillars = set() + for test in principle_tests: + for pillar in TESTS_MAP[test][PILLARS_KEY]: + principles_pillars.add(pillar) + PRINCIPLES_TO_PILLARS[principle] = principles_pillars diff --git a/monkey/common/network/segmentation_utils.py b/monkey/common/network/segmentation_utils.py new file mode 100644 index 000000000..9bbaabf1d --- /dev/null +++ b/monkey/common/network/segmentation_utils.py @@ -0,0 +1,23 @@ +def get_ip_in_src_and_not_in_dst(ip_addresses, source_subnet, target_subnet): + """ + Finds an IP address in ip_addresses which is in source_subnet but not in target_subnet. + :param ip_addresses: List[str]: List of IP addresses to test. + :param source_subnet: NetworkRange: Subnet to want an IP to not be in. + :param target_subnet: NetworkRange: Subnet we want an IP to be in. + :return: The cross segment IP if in source but not in target, else None. Union[str, None] + """ + if get_ip_if_in_subnet(ip_addresses, target_subnet) is not None: + return None + return 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 subnet.is_in_range(ip_address): + return ip_address + return None diff --git a/monkey/common/network/segmentation_utils_test.py b/monkey/common/network/segmentation_utils_test.py new file mode 100644 index 000000000..56a560922 --- /dev/null +++ b/monkey/common/network/segmentation_utils_test.py @@ -0,0 +1,30 @@ +from common.network.network_range import * +from common.network.segmentation_utils import get_ip_in_src_and_not_in_dst +from monkey_island.cc.testing.IslandTestCase import IslandTestCase + + +class TestSegmentationUtils(IslandTestCase): + def test_get_ip_in_src_and_not_in_dst(self): + self.fail_if_not_testing_env() + source = CidrRange("1.1.1.0/24") + target = CidrRange("2.2.2.0/24") + + # 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 + )) diff --git a/monkey/infection_monkey/exploit/elasticgroovy.py b/monkey/infection_monkey/exploit/elasticgroovy.py index 39fdf29b1..f1057f2dd 100644 --- a/monkey/infection_monkey/exploit/elasticgroovy.py +++ b/monkey/infection_monkey/exploit/elasticgroovy.py @@ -10,7 +10,8 @@ import requests from infection_monkey.exploit.web_rce import WebRCE 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 diff --git a/monkey/infection_monkey/exploit/mssqlexec.py b/monkey/infection_monkey/exploit/mssqlexec.py index db503c717..718615114 100644 --- a/monkey/infection_monkey/exploit/mssqlexec.py +++ b/monkey/infection_monkey/exploit/mssqlexec.py @@ -10,6 +10,7 @@ from infection_monkey.exploit import HostExploiter from infection_monkey.exploit.tools.http_tools import MonkeyHTTPServer from infection_monkey.exploit.tools.helpers import get_monkey_dest_path, build_monkey_commandline, get_monkey_depth from infection_monkey.model import DROPPER_ARG +from infection_monkey.utils.monkey_dir import get_monkey_dir_path from infection_monkey.exploit.tools.payload_parsing import LimitedSizePayload from infection_monkey.exploit.tools.exceptions import ExploitingVulnerableMachineError diff --git a/monkey/infection_monkey/monkey_utils/sambacry_monkey_runner/build.sh b/monkey/infection_monkey/exploit/sambacry_monkey_runner/build.sh similarity index 100% rename from monkey/infection_monkey/monkey_utils/sambacry_monkey_runner/build.sh rename to monkey/infection_monkey/exploit/sambacry_monkey_runner/build.sh diff --git a/monkey/infection_monkey/monkey_utils/sambacry_monkey_runner/sc_monkey_runner.c b/monkey/infection_monkey/exploit/sambacry_monkey_runner/sc_monkey_runner.c similarity index 100% rename from monkey/infection_monkey/monkey_utils/sambacry_monkey_runner/sc_monkey_runner.c rename to monkey/infection_monkey/exploit/sambacry_monkey_runner/sc_monkey_runner.c diff --git a/monkey/infection_monkey/monkey_utils/sambacry_monkey_runner/sc_monkey_runner.h b/monkey/infection_monkey/exploit/sambacry_monkey_runner/sc_monkey_runner.h similarity index 100% rename from monkey/infection_monkey/monkey_utils/sambacry_monkey_runner/sc_monkey_runner.h rename to monkey/infection_monkey/exploit/sambacry_monkey_runner/sc_monkey_runner.h diff --git a/monkey/infection_monkey/main.py b/monkey/infection_monkey/main.py index 2ddf9127e..c20a84190 100644 --- a/monkey/infection_monkey/main.py +++ b/monkey/infection_monkey/main.py @@ -8,7 +8,7 @@ import os import sys import traceback -import infection_monkey.utils as utils +from infection_monkey.utils.monkey_log_path import get_dropper_log_path, get_monkey_log_path from infection_monkey.config import WormConfiguration, EXTERNAL_CONFIG_FILE from infection_monkey.dropper import MonkeyDrops from infection_monkey.model import MONKEY_ARG, DROPPER_ARG @@ -79,10 +79,10 @@ def main(): try: if MONKEY_ARG == monkey_mode: - log_path = utils.get_monkey_log_path() + log_path = get_monkey_log_path() monkey_cls = InfectionMonkey elif DROPPER_ARG == monkey_mode: - log_path = utils.get_dropper_log_path() + log_path = get_dropper_log_path() monkey_cls = MonkeyDrops else: return True @@ -127,8 +127,8 @@ def main(): json.dump(json_dict, config_fo, skipkeys=True, sort_keys=True, indent=4, separators=(',', ': ')) return True - except Exception: - LOG.exception("Exception thrown from monkey's start function") + except Exception as e: + LOG.exception("Exception thrown from monkey's start function. More info: {}".format(e)) finally: monkey.cleanup() diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index cd8df4705..41e8ae556 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -7,7 +7,8 @@ import time from six.moves import xrange import infection_monkey.tunnel as tunnel -import infection_monkey.utils as utils +from infection_monkey.utils.monkey_dir import create_monkey_dir, get_monkey_dir_path, remove_monkey_dir +from infection_monkey.utils.monkey_log_path import get_monkey_log_path from infection_monkey.config import WormConfiguration from infection_monkey.control import ControlClient from infection_monkey.model import DELAY_DELETE_CMD @@ -91,7 +92,7 @@ class InfectionMonkey(object): self.set_default_port() # Create a dir for monkey files if there isn't one - utils.create_monkey_dir() + create_monkey_dir() if WindowsUpgrader.should_upgrade(): self._upgrading_to_64 = True @@ -114,7 +115,7 @@ class InfectionMonkey(object): if monkey_tunnel: monkey_tunnel.start() - StateTelem(False).send() + StateTelem(is_done=False).send() TunnelTelem().send() if WormConfiguration.collect_system_info: @@ -226,7 +227,7 @@ class InfectionMonkey(object): InfectionMonkey.close_tunnel() firewall.close() else: - StateTelem(True).send() # Signal the server (before closing the tunnel) + StateTelem(is_done=True).send() # Signal the server (before closing the tunnel) InfectionMonkey.close_tunnel() firewall.close() if WormConfiguration.send_log_to_server: @@ -245,8 +246,8 @@ class InfectionMonkey(object): @staticmethod def self_delete(): - status = ScanStatus.USED if utils.remove_monkey_dir() else ScanStatus.SCANNED - T1107Telem(status, utils.get_monkey_dir_path()).send() + status = ScanStatus.USED if remove_monkey_dir() else ScanStatus.SCANNED + T1107Telem(status, get_monkey_dir_path()).send() if WormConfiguration.self_delete_in_cleanup \ and -1 == sys.executable.find('python'): @@ -270,7 +271,7 @@ class InfectionMonkey(object): T1107Telem(status, sys.executable).send() def send_log(self): - monkey_log_path = utils.get_monkey_log_path() + monkey_log_path = get_monkey_log_path() if os.path.exists(monkey_log_path): with open(monkey_log_path, 'r') as f: log = f.read() diff --git a/monkey/infection_monkey/network/elasticfinger.py b/monkey/infection_monkey/network/elasticfinger.py index 31ce6e24a..aaac09be2 100644 --- a/monkey/infection_monkey/network/elasticfinger.py +++ b/monkey/infection_monkey/network/elasticfinger.py @@ -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' diff --git a/monkey/infection_monkey/network/tools.py b/monkey/infection_monkey/network/tools.py index 3a9adef57..5e448002c 100644 --- a/monkey/infection_monkey/network/tools.py +++ b/monkey/infection_monkey/network/tools.py @@ -10,7 +10,7 @@ import re from six.moves import range from infection_monkey.pyinstaller_utils import get_binary_file_path -from infection_monkey.utils import is_64bit_python +from infection_monkey.utils.environment import is_64bit_python DEFAULT_TIMEOUT = 10 BANNER_READ = 1024 diff --git a/monkey/infection_monkey/post_breach/actions/add_user.py b/monkey/infection_monkey/post_breach/actions/add_user.py index ff7ae3a50..09c8d4796 100644 --- a/monkey/infection_monkey/post_breach/actions/add_user.py +++ b/monkey/infection_monkey/post_breach/actions/add_user.py @@ -1,21 +1,16 @@ -import datetime +from common.data.post_breach_consts import POST_BREACH_BACKDOOR_USER from infection_monkey.post_breach.pba import PBA from infection_monkey.config import WormConfiguration - - -__author__ = 'danielg' - -LINUX_COMMANDS = ['useradd', '-M', '--expiredate', - datetime.datetime.today().strftime('%Y-%m-%d'), '--inactive', '0', '-c', 'MONKEY_USER', - WormConfiguration.user_to_add] - -WINDOWS_COMMANDS = ['net', 'user', WormConfiguration.user_to_add, - WormConfiguration.remote_user_pass, - '/add', '/ACTIVE:NO'] +from infection_monkey.utils.users import get_commands_to_add_user class BackdoorUser(PBA): def __init__(self): - super(BackdoorUser, self).__init__("Backdoor user", - linux_cmd=' '.join(LINUX_COMMANDS), - windows_cmd=WINDOWS_COMMANDS) + linux_cmds, windows_cmds = get_commands_to_add_user( + WormConfiguration.user_to_add, + WormConfiguration.remote_user_pass) + super(BackdoorUser, self).__init__( + POST_BREACH_BACKDOOR_USER, + linux_cmd=' '.join(linux_cmds), + windows_cmd=windows_cmds) + diff --git a/monkey/infection_monkey/post_breach/actions/communicate_as_new_user.py b/monkey/infection_monkey/post_breach/actions/communicate_as_new_user.py new file mode 100644 index 000000000..296179d41 --- /dev/null +++ b/monkey/infection_monkey/post_breach/actions/communicate_as_new_user.py @@ -0,0 +1,143 @@ +import logging +import os +import random +import string +import subprocess +import time + +import win32event + +from infection_monkey.utils.windows.auto_new_user import AutoNewUser, NewUserError +from common.data.post_breach_consts import POST_BREACH_COMMUNICATE_AS_NEW_USER +from infection_monkey.post_breach.pba import PBA +from infection_monkey.telemetry.post_breach_telem import PostBreachTelem +from infection_monkey.utils.environment import is_windows_os +from infection_monkey.utils.linux.users import get_linux_commands_to_delete_user, get_linux_commands_to_add_user + +PING_TEST_DOMAIN = "google.com" + +PING_WAIT_TIMEOUT_IN_MILLISECONDS = 20 * 1000 + +CREATED_PROCESS_AS_USER_PING_SUCCESS_FORMAT = "Created process '{}' as user '{}', and successfully pinged." +CREATED_PROCESS_AS_USER_PING_FAILED_FORMAT = "Created process '{}' as user '{}', but failed to ping (exit status {})." + +USERNAME = "somenewuser" +PASSWORD = "N3WPa55W0rD!1" + +logger = logging.getLogger(__name__) + + +class CommunicateAsNewUser(PBA): + """ + This PBA creates a new user, and then pings google as that user. This is used for a Zero Trust test of the People + pillar. See the relevant telemetry processing to see what findings are created. + """ + + def __init__(self): + super(CommunicateAsNewUser, self).__init__(name=POST_BREACH_COMMUNICATE_AS_NEW_USER) + + def run(self): + username = CommunicateAsNewUser.get_random_new_user_name() + if is_windows_os(): + self.communicate_as_new_user_windows(username) + else: + self.communicate_as_new_user_linux(username) + + @staticmethod + def get_random_new_user_name(): + return USERNAME + ''.join(random.choice(string.ascii_lowercase) for _ in range(5)) + + def communicate_as_new_user_linux(self, username): + try: + # add user + ping + linux_cmds = get_linux_commands_to_add_user(username) + commandline = "ping -c 1 {}".format(PING_TEST_DOMAIN) + linux_cmds.extend([";", "sudo", "-u", username, commandline]) + final_command = ' '.join(linux_cmds) + exit_status = os.system(final_command) + self.send_ping_result_telemetry(exit_status, commandline, username) + # delete the user, async in case it gets stuck. + _ = subprocess.Popen( + get_linux_commands_to_delete_user(username), stderr=subprocess.STDOUT, shell=True) + # Leaking the process on purpose - nothing we can do if it's stuck. + except subprocess.CalledProcessError as e: + PostBreachTelem(self, (e.output, False)).send() + + def communicate_as_new_user_windows(self, username): + # Importing these only on windows, as they won't exist on linux. + import win32con + import win32process + import win32api + + try: + with AutoNewUser(username, PASSWORD) as new_user: + # Using os.path is OK, as this is on windows for sure + ping_app_path = os.path.join(os.environ["WINDIR"], "system32", "PING.exe") + if not os.path.exists(ping_app_path): + PostBreachTelem(self, ("{} not found.".format(ping_app_path), False)).send() + return # Can't continue without ping. + + try: + # Open process as that user: + # https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createprocessasusera + commandline = "{} {} {} {}".format(ping_app_path, PING_TEST_DOMAIN, "-n", "1") + process_handle, thread_handle, _, _ = win32process.CreateProcessAsUser( + new_user.get_logon_handle(), # A handle to the primary token that represents a user. + None, # The name of the module to be executed. + commandline, # The command line to be executed. + None, # Process attributes + None, # Thread attributes + True, # Should inherit handles + win32con.NORMAL_PRIORITY_CLASS, # The priority class and the creation of the process. + None, # An environment block for the new process. If this parameter is NULL, the new process + # uses the environment of the calling process. + None, # CWD. If this parameter is NULL, the new process will have the same current drive and + # directory as the calling process. + win32process.STARTUPINFO() # STARTUPINFO structure. + # https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/ns-processthreadsapi-startupinfoa + ) + + logger.debug( + "Waiting for ping process to finish. Timeout: {}ms".format(PING_WAIT_TIMEOUT_IN_MILLISECONDS)) + + # Ignoring return code, as we'll use `GetExitCode` to determine the state of the process later. + _ = win32event.WaitForSingleObject( # Waits until the specified object is signaled, or time-out. + process_handle, # Ping process handle + PING_WAIT_TIMEOUT_IN_MILLISECONDS # Timeout in milliseconds + ) + + ping_exit_code = win32process.GetExitCodeProcess(process_handle) + + self.send_ping_result_telemetry(ping_exit_code, commandline, username) + except Exception as e: + # If failed on 1314, it's possible to try to elevate the rights of the current user with the + # "Replace a process level token" right, using Local Security Policy editing. + PostBreachTelem(self, ( + "Failed to open process as user {}. Error: {}".format(username, str(e)), False)).send() + finally: + try: + win32api.CloseHandle(process_handle) + win32api.CloseHandle(thread_handle) + except Exception as err: + logger.error("Close handle error: " + str(err)) + except subprocess.CalledProcessError as err: + PostBreachTelem(self, ( + "Couldn't create the user '{}'. Error output is: '{}'".format(username, str(err)), + False)).send() + except NewUserError as e: + PostBreachTelem(self, (str(e), False)).send() + + def send_ping_result_telemetry(self, exit_status, commandline, username): + """ + Parses the result of ping and sends telemetry accordingly. + + :param exit_status: In both Windows and Linux, 0 exit code from Ping indicates success. + :param commandline: Exact commandline which was executed, for reporting back. + :param username: Username from which the command was executed, for reporting back. + """ + if exit_status == 0: + PostBreachTelem(self, ( + CREATED_PROCESS_AS_USER_PING_SUCCESS_FORMAT.format(commandline, username), True)).send() + else: + PostBreachTelem(self, ( + CREATED_PROCESS_AS_USER_PING_FAILED_FORMAT.format(commandline, username, exit_status), False)).send() diff --git a/monkey/infection_monkey/post_breach/actions/users_custom_pba.py b/monkey/infection_monkey/post_breach/actions/users_custom_pba.py index 118868d0c..89417757d 100644 --- a/monkey/infection_monkey/post_breach/actions/users_custom_pba.py +++ b/monkey/infection_monkey/post_breach/actions/users_custom_pba.py @@ -1,11 +1,12 @@ import os import logging -from infection_monkey.utils import is_windows_os +from common.data.post_breach_consts import POST_BREACH_FILE_EXECUTION +from infection_monkey.utils.environment import is_windows_os from infection_monkey.post_breach.pba import PBA from infection_monkey.control import ControlClient from infection_monkey.config import WormConfiguration -from infection_monkey.utils import get_monkey_dir_path +from infection_monkey.utils.monkey_dir import get_monkey_dir_path from infection_monkey.telemetry.attack.t1105_telem import T1105Telem from common.utils.attack_utils import ScanStatus from infection_monkey.exploit.tools.helpers import get_interface_to_target @@ -27,7 +28,7 @@ class UsersPBA(PBA): Defines user's configured post breach action. """ def __init__(self): - super(UsersPBA, self).__init__("Custom post breach action") + super(UsersPBA, self).__init__(POST_BREACH_FILE_EXECUTION) self.filename = '' if not is_windows_os(): # Add linux commands to PBA's diff --git a/monkey/infection_monkey/post_breach/pba.py b/monkey/infection_monkey/post_breach/pba.py index 926594192..8d7723df2 100644 --- a/monkey/infection_monkey/post_breach/pba.py +++ b/monkey/infection_monkey/post_breach/pba.py @@ -3,7 +3,7 @@ import subprocess 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.utils.environment import is_windows_os from infection_monkey.config import WormConfiguration from infection_monkey.telemetry.attack.t1064_telem import T1064Telem @@ -21,7 +21,8 @@ class PBA(object): def __init__(self, name="unknown", linux_cmd="", windows_cmd=""): """ :param name: Name of post breach action. - :param command: Command that will be executed on breached machine + :param linux_cmd: Command that will be executed on breached machine + :param windows_cmd: Command that will be executed on breached machine """ self.command = PBA.choose_command(linux_cmd, windows_cmd) self.name = name diff --git a/monkey/infection_monkey/post_breach/post_breach_handler.py b/monkey/infection_monkey/post_breach/post_breach_handler.py index 8522f412f..b5dfa93c7 100644 --- a/monkey/infection_monkey/post_breach/post_breach_handler.py +++ b/monkey/infection_monkey/post_breach/post_breach_handler.py @@ -3,7 +3,7 @@ import inspect import importlib from infection_monkey.post_breach.pba import PBA from infection_monkey.post_breach.actions import get_pba_files -from infection_monkey.utils import is_windows_os +from infection_monkey.utils.environment import is_windows_os LOG = logging.getLogger(__name__) @@ -25,8 +25,12 @@ class PostBreach(object): Executes all post breach actions. """ for pba in self.pba_list: - pba.run() - LOG.info("Post breach actions executed") + try: + LOG.debug("Executing PBA: '{}'".format(pba.name)) + pba.run() + except Exception as e: + LOG.error("PBA {} failed. Error info: {}".format(pba.name, e)) + LOG.info("All PBAs executed. Total {} executed.".format(len(self.pba_list))) @staticmethod def config_to_pba_list(): @@ -45,7 +49,9 @@ class PostBreach(object): if ((m[1].__module__ == module.__name__) and issubclass(m[1], PBA))] # Get post breach action object from class for pba_class in pba_classes: + LOG.debug("Checking if should run PBA {}".format(pba_class.__name__)) if pba_class.should_run(pba_class.__name__): pba = pba_class() pba_list.append(pba) + LOG.debug("Added PBA {} to PBA list".format(pba_class.__name__)) return pba_list diff --git a/monkey/infection_monkey/readme.txt b/monkey/infection_monkey/readme.txt index 0b56da2f7..06bf449da 100644 --- a/monkey/infection_monkey/readme.txt +++ b/monkey/infection_monkey/readme.txt @@ -62,7 +62,7 @@ a. Build sambacry binaries yourself a.1. Install gcc-multilib if it's not installed sudo apt-get install gcc-multilib a.2. Build the binaries - cd [code location]/infection_monkey/monkey_utils/sambacry_monkey_runner + cd [code location]/infection_monkey/exploit/sambacry_monkey_runner ./build.sh b. Download our pre-built sambacry binaries diff --git a/monkey/infection_monkey/telemetry/base_telem.py b/monkey/infection_monkey/telemetry/base_telem.py index c232ab975..31d7332bd 100644 --- a/monkey/infection_monkey/telemetry/base_telem.py +++ b/monkey/infection_monkey/telemetry/base_telem.py @@ -1,7 +1,11 @@ import abc +import json +import logging from infection_monkey.control import ControlClient +logger = logging.getLogger(__name__) + __author__ = 'itay.mizeretz' @@ -19,7 +23,9 @@ class BaseTelem(object): """ Sends telemetry to island """ - ControlClient.send_telemetry(self.telem_category, self.get_data()) + data = self.get_data() + logger.debug("Sending {} telemetry. Data: {}".format(self.telem_category, json.dumps(data))) + ControlClient.send_telemetry(self.telem_category, data) @abc.abstractproperty def telem_category(self): diff --git a/monkey/infection_monkey/utils.py b/monkey/infection_monkey/utils.py deleted file mode 100644 index f8b5cc56a..000000000 --- a/monkey/infection_monkey/utils.py +++ /dev/null @@ -1,62 +0,0 @@ -import os -import shutil -import struct -import sys -import tempfile - -from infection_monkey.config import WormConfiguration - - -def get_monkey_log_path(): - return os.path.expandvars(WormConfiguration.monkey_log_path_windows) if sys.platform == "win32" \ - else WormConfiguration.monkey_log_path_linux - - -def get_dropper_log_path(): - return os.path.expandvars(WormConfiguration.dropper_log_path_windows) if sys.platform == "win32" \ - else WormConfiguration.dropper_log_path_linux - - -def is_64bit_windows_os(): - """ - Checks for 64 bit Windows OS using environment variables. - """ - return 'PROGRAMFILES(X86)' in os.environ - - -def is_64bit_python(): - return struct.calcsize("P") == 8 - - -def is_windows_os(): - return sys.platform.startswith("win") - - -def utf_to_ascii(string): - # Converts utf string to ascii. Safe to use even if string is already ascii. - udata = string.decode("utf-8") - return udata.encode("ascii", "ignore") - - -def create_monkey_dir(): - """ - Creates directory for monkey and related files - """ - if not os.path.exists(get_monkey_dir_path()): - os.mkdir(get_monkey_dir_path()) - - -def remove_monkey_dir(): - """ - Removes monkey's root directory - :return True if removed without errors and False otherwise - """ - try: - shutil.rmtree(get_monkey_dir_path()) - return True - except Exception: - return False - - -def get_monkey_dir_path(): - return os.path.join(tempfile.gettempdir(), WormConfiguration.monkey_dir_name) diff --git a/monkey/infection_monkey/utils/__init__.py b/monkey/infection_monkey/utils/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/monkey/infection_monkey/utils/environment.py b/monkey/infection_monkey/utils/environment.py new file mode 100644 index 000000000..40a70ce58 --- /dev/null +++ b/monkey/infection_monkey/utils/environment.py @@ -0,0 +1,18 @@ +import os +import struct +import sys + + +def is_64bit_windows_os(): + """ + Checks for 64 bit Windows OS using environment variables. + """ + return 'PROGRAMFILES(X86)' in os.environ + + +def is_64bit_python(): + return struct.calcsize("P") == 8 + + +def is_windows_os(): + return sys.platform.startswith("win") diff --git a/monkey/infection_monkey/utils/linux/__init__.py b/monkey/infection_monkey/utils/linux/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/monkey/infection_monkey/utils/linux/users.py b/monkey/infection_monkey/utils/linux/users.py new file mode 100644 index 000000000..1acc87d72 --- /dev/null +++ b/monkey/infection_monkey/utils/linux/users.py @@ -0,0 +1,21 @@ +import datetime + + +def get_linux_commands_to_add_user(username): + return [ + 'useradd', + '-M', # Do not create homedir + '--expiredate', + datetime.datetime.today().strftime('%Y-%m-%d'), + '--inactive', + '0', + '-c', # Comment + 'MONKEY_USER', # Comment + username] + + +def get_linux_commands_to_delete_user(username): + return [ + 'deluser', + username + ] diff --git a/monkey/infection_monkey/utils/monkey_dir.py b/monkey/infection_monkey/utils/monkey_dir.py new file mode 100644 index 000000000..bb69dae5b --- /dev/null +++ b/monkey/infection_monkey/utils/monkey_dir.py @@ -0,0 +1,29 @@ +import os +import shutil +import tempfile + +from infection_monkey.config import WormConfiguration + + +def create_monkey_dir(): + """ + Creates directory for monkey and related files + """ + if not os.path.exists(get_monkey_dir_path()): + os.mkdir(get_monkey_dir_path()) + + +def remove_monkey_dir(): + """ + Removes monkey's root directory + :return True if removed without errors and False otherwise + """ + try: + shutil.rmtree(get_monkey_dir_path()) + return True + except Exception: + return False + + +def get_monkey_dir_path(): + return os.path.join(tempfile.gettempdir(), WormConfiguration.monkey_dir_name) diff --git a/monkey/infection_monkey/utils/monkey_log_path.py b/monkey/infection_monkey/utils/monkey_log_path.py new file mode 100644 index 000000000..ad80bc73d --- /dev/null +++ b/monkey/infection_monkey/utils/monkey_log_path.py @@ -0,0 +1,14 @@ +import os +import sys + +from infection_monkey.config import WormConfiguration + + +def get_monkey_log_path(): + return os.path.expandvars(WormConfiguration.monkey_log_path_windows) if sys.platform == "win32" \ + else WormConfiguration.monkey_log_path_linux + + +def get_dropper_log_path(): + return os.path.expandvars(WormConfiguration.dropper_log_path_windows) if sys.platform == "win32" \ + else WormConfiguration.dropper_log_path_linux diff --git a/monkey/infection_monkey/utils/users.py b/monkey/infection_monkey/utils/users.py new file mode 100644 index 000000000..68148d9e9 --- /dev/null +++ b/monkey/infection_monkey/utils/users.py @@ -0,0 +1,10 @@ +from infection_monkey.utils.linux.users import get_linux_commands_to_add_user +from infection_monkey.utils.windows.users import get_windows_commands_to_add_user + + +def get_commands_to_add_user(username, password): + linux_cmds = get_linux_commands_to_add_user(username) + windows_cmds = get_windows_commands_to_add_user(username, password) + return linux_cmds, windows_cmds + + diff --git a/monkey/infection_monkey/utils/windows/__init__.py b/monkey/infection_monkey/utils/windows/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/monkey/infection_monkey/utils/windows/auto_new_user.py b/monkey/infection_monkey/utils/windows/auto_new_user.py new file mode 100644 index 000000000..d95ac0bf0 --- /dev/null +++ b/monkey/infection_monkey/utils/windows/auto_new_user.py @@ -0,0 +1,69 @@ +import logging +import subprocess + +from infection_monkey.post_breach.actions.add_user import BackdoorUser +from infection_monkey.utils.windows.users import get_windows_commands_to_delete_user, get_windows_commands_to_add_user + +logger = logging.getLogger(__name__) + + +class NewUserError(Exception): + pass + + +class AutoNewUser(object): + """ + RAII object to use for creating and using a new user in Windows. Use with `with`. + User will be created when the instance is instantiated. + User will log on at the start of the `with` scope. + User will log off and get deleted at the end of said `with` scope. + + Example: + # Created # Logged on + with AutoNewUser("user", "pass") as new_user: + ... + ... + # Logged off and deleted + ... + """ + def __init__(self, username, password): + """ + Creates a user with the username + password. + :raises: subprocess.CalledProcessError if failed to add the user. + """ + self.username = username + self.password = password + + windows_cmds = get_windows_commands_to_add_user(self.username, self.password, True) + _ = subprocess.check_output(windows_cmds, stderr=subprocess.STDOUT, shell=True) + + def __enter__(self): + # Importing these only on windows, as they won't exist on linux. + import win32security + import win32con + + try: + # Logon as new user: https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-logonusera + self.logon_handle = win32security.LogonUser( + self.username, + ".", # Use current domain. + self.password, + win32con.LOGON32_LOGON_INTERACTIVE, # Logon type - interactive (normal user). + win32con.LOGON32_PROVIDER_DEFAULT) # Which logon provider to use - whatever Windows offers. + except Exception as err: + raise NewUserError("Can't logon as {}. Error: {}".format(self.username, str(err))) + return self + + def get_logon_handle(self): + return self.logon_handle + + def __exit__(self, exc_type, exc_val, exc_tb): + # Logoff + self.logon_handle.Close() + + # Try to delete user + try: + _ = subprocess.Popen( + get_windows_commands_to_delete_user(self.username), stderr=subprocess.STDOUT, shell=True) + except Exception as err: + raise NewUserError("Can't delete user {}. Info: {}".format(self.username, err)) diff --git a/monkey/infection_monkey/utils/windows/users.py b/monkey/infection_monkey/utils/windows/users.py new file mode 100644 index 000000000..0e6847cff --- /dev/null +++ b/monkey/infection_monkey/utils/windows/users.py @@ -0,0 +1,18 @@ +def get_windows_commands_to_add_user(username, password, should_be_active=False): + windows_cmds = [ + 'net', + 'user', + username, + password, + '/add'] + if not should_be_active: + windows_cmds.append('/ACTIVE:NO') + return windows_cmds + + +def get_windows_commands_to_delete_user(username): + return [ + 'net', + 'user', + username, + '/delete'] diff --git a/monkey/infection_monkey/windows_upgrader.py b/monkey/infection_monkey/windows_upgrader.py index 4a165940d..af904b143 100644 --- a/monkey/infection_monkey/windows_upgrader.py +++ b/monkey/infection_monkey/windows_upgrader.py @@ -10,7 +10,7 @@ from infection_monkey.config import WormConfiguration from infection_monkey.control import ControlClient from infection_monkey.exploit.tools.helpers import build_monkey_commandline_explicitly from infection_monkey.model import MONKEY_CMDLINE_WINDOWS -from infection_monkey.utils import is_windows_os, is_64bit_windows_os, is_64bit_python +from infection_monkey.utils.environment import is_windows_os, is_64bit_windows_os, is_64bit_python __author__ = 'itay.mizeretz' diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index 2e04ef0be..35e932cb1 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -23,7 +23,7 @@ from monkey_island.cc.resources.monkey_download import MonkeyDownload from monkey_island.cc.resources.netmap import NetMap from monkey_island.cc.resources.node import Node from monkey_island.cc.resources.remote_run import RemoteRun -from monkey_island.cc.resources.report import Report +from monkey_island.cc.resources.reporting.report import Report from monkey_island.cc.resources.root import Root from monkey_island.cc.resources.telemetry import Telemetry from monkey_island.cc.resources.telemetry_feed import TelemetryFeed @@ -122,7 +122,13 @@ def init_api_resources(api): api.add_resource(NetMap, '/api/netmap', '/api/netmap/') api.add_resource(Edge, '/api/netmap/edge', '/api/netmap/edge/') api.add_resource(Node, '/api/netmap/node', '/api/netmap/node/') - api.add_resource(Report, '/api/report', '/api/report/') + + # report_type: zero_trust or security + api.add_resource( + Report, + '/api/report/', + '/api/report//') + api.add_resource(TelemetryFeed, '/api/telemetry-feed', '/api/telemetry-feed/') api.add_resource(Log, '/api/log', '/api/log/') api.add_resource(IslandLog, '/api/log/island/download', '/api/log/island/download/') diff --git a/monkey/monkey_island/cc/environment/testing.py b/monkey/monkey_island/cc/environment/testing.py index 286e442dd..087c3a2e3 100644 --- a/monkey/monkey_island/cc/environment/testing.py +++ b/monkey/monkey_island/cc/environment/testing.py @@ -1,5 +1,4 @@ from monkey_island.cc.environment import Environment -import monkey_island.cc.auth class TestingEnvironment(Environment): @@ -7,11 +6,5 @@ class TestingEnvironment(Environment): super(TestingEnvironment, self).__init__() self.testing = True - # SHA3-512 of '1234567890!@#$%^&*()_nothing_up_my_sleeve_1234567890!@#$%^&*()' - NO_AUTH_CREDS = '55e97c9dcfd22b8079189ddaeea9bce8125887e3237b800c6176c9afa80d2062' \ - '8d2c8d0b1538d2208c1444ac66535b764a3d902b35e751df3faec1e477ed3557' - def get_auth_users(self): - return [ - monkey_island.cc.auth.User(1, self.NO_AUTH_CREDS, self.NO_AUTH_CREDS) - ] + return [] diff --git a/monkey/monkey_island/cc/models/zero_trust/__init__.py b/monkey/monkey_island/cc/models/zero_trust/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/monkey/monkey_island/cc/models/zero_trust/aggregate_finding.py b/monkey/monkey_island/cc/models/zero_trust/aggregate_finding.py new file mode 100644 index 000000000..c3ed52649 --- /dev/null +++ b/monkey/monkey_island/cc/models/zero_trust/aggregate_finding.py @@ -0,0 +1,32 @@ +from common.data.zero_trust_consts import TEST_MALICIOUS_ACTIVITY_TIMELINE, STATUS_VERIFY +from monkey_island.cc.models.zero_trust.finding import Finding + + +class AggregateFinding(Finding): + @staticmethod + def create_or_add_to_existing(test, status, events): + """ + Create a new finding or add the events to an existing one if it's the same (same meaning same status and same + test). + + :raises: Assertion error if this is used when there's more then one finding which fits the query - this is not + when this function should be used. + """ + existing_findings = Finding.objects(test=test, status=status) + assert (len(existing_findings) < 2), "More than one finding exists for {}:{}".format(test, status) + + if len(existing_findings) == 0: + Finding.save_finding(test, status, events) + else: + # Now we know for sure this is the only one + orig_finding = existing_findings[0] + orig_finding.add_events(events) + orig_finding.save() + + +def add_malicious_activity_to_timeline(events): + AggregateFinding.create_or_add_to_existing( + test=TEST_MALICIOUS_ACTIVITY_TIMELINE, + status=STATUS_VERIFY, + events=events + ) diff --git a/monkey/monkey_island/cc/models/zero_trust/event.py b/monkey/monkey_island/cc/models/zero_trust/event.py new file mode 100644 index 000000000..6ad728d66 --- /dev/null +++ b/monkey/monkey_island/cc/models/zero_trust/event.py @@ -0,0 +1,36 @@ +from datetime import datetime + +from mongoengine import EmbeddedDocument, DateTimeField, StringField + +from common.data.zero_trust_consts import EVENT_TYPES + + +class Event(EmbeddedDocument): + """ + This model represents a single event within a Finding (it is an EmbeddedDocument within Finding). It is meant to + hold a detail of the Finding. + + This class has 2 main section: + * The schema section defines the DB fields in the document. This is the data of the object. + * The logic section defines complex questions we can ask about a single document which are asked multiple + times, or complex action we will perform - somewhat like an API. + """ + # SCHEMA + timestamp = DateTimeField(required=True) + title = StringField(required=True) + message = StringField() + event_type = StringField(required=True, choices=EVENT_TYPES) + + # LOGIC + @staticmethod + def create_event(title, message, event_type, timestamp=datetime.now()): + event = Event( + timestamp=timestamp, + title=title, + message=message, + event_type=event_type + ) + + event.validate(clean=True) + + return event diff --git a/monkey/monkey_island/cc/models/zero_trust/finding.py b/monkey/monkey_island/cc/models/zero_trust/finding.py new file mode 100644 index 000000000..df4eb12f7 --- /dev/null +++ b/monkey/monkey_island/cc/models/zero_trust/finding.py @@ -0,0 +1,60 @@ +# coding=utf-8 +""" +Define a Document Schema for Zero Trust findings. +""" + +from mongoengine import Document, StringField, EmbeddedDocumentListField + +from common.data.zero_trust_consts import ORDERED_TEST_STATUSES, TESTS, TESTS_MAP, TEST_EXPLANATION_KEY, PILLARS_KEY +# Dummy import for mongoengine. +# noinspection PyUnresolvedReferences +from monkey_island.cc.models.zero_trust.event import Event + + +class Finding(Document): + """ + This model represents a Zero-Trust finding: A result of a test the monkey/island might perform to see if a + specific principle of zero trust is upheld or broken. + + Findings might have the following statuses: + Failed ❌ + Meaning that we are sure that something is wrong (example: segmentation issue). + Verify ⁉ + Meaning that we need the user to check something himself (example: 2FA logs, AV missing). + Passed ✔ + Meaning that we are sure that something is correct (example: Monkey failed exploiting). + + This class has 2 main section: + * The schema section defines the DB fields in the document. This is the data of the object. + * The logic section defines complex questions we can ask about a single document which are asked multiple + times, or complex action we will perform - somewhat like an API. + """ + # SCHEMA + test = StringField(required=True, choices=TESTS) + status = StringField(required=True, choices=ORDERED_TEST_STATUSES) + events = EmbeddedDocumentListField(document_type=Event) + # http://docs.mongoengine.org/guide/defining-documents.html#document-inheritance + meta = {'allow_inheritance': True} + + # LOGIC + def get_test_explanation(self): + return TESTS_MAP[self.test][TEST_EXPLANATION_KEY] + + def get_pillars(self): + return TESTS_MAP[self.test][PILLARS_KEY] + + # Creation methods + @staticmethod + def save_finding(test, status, events): + finding = Finding( + test=test, + status=status, + events=events) + + finding.save() + + return finding + + def add_events(self, events): + # type: (list) -> None + self.events.extend(events) diff --git a/monkey/monkey_island/cc/models/zero_trust/segmentation_finding.py b/monkey/monkey_island/cc/models/zero_trust/segmentation_finding.py new file mode 100644 index 000000000..32a450f57 --- /dev/null +++ b/monkey/monkey_island/cc/models/zero_trust/segmentation_finding.py @@ -0,0 +1,50 @@ +from mongoengine import StringField + +from common.data.zero_trust_consts import TEST_SEGMENTATION, STATUS_FAILED, STATUS_PASSED +from monkey_island.cc.models.zero_trust.finding import Finding + + +def need_to_overwrite_status(saved_status, new_status): + return (saved_status == STATUS_PASSED) and (new_status == STATUS_FAILED) + + +class SegmentationFinding(Finding): + first_subnet = StringField() + second_subnet = StringField() + + @staticmethod + def create_or_add_to_existing_finding(subnets, status, segmentation_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 + :param segmentation_event: The specific event + """ + assert len(subnets) == 2 + + # Sort them so A -> B and B -> A segmentation findings will be the same one. + subnets.sort() + + existing_findings = SegmentationFinding.objects(first_subnet=subnets[0], second_subnet=subnets[1]) + + if len(existing_findings) == 0: + # No finding exists - create. + new_finding = SegmentationFinding( + first_subnet=subnets[0], + second_subnet=subnets[1], + test=TEST_SEGMENTATION, + status=status, + events=[segmentation_event] + ) + new_finding.save() + else: + # A finding exists (should be one). Add the event to it. + assert len(existing_findings) == 1 + existing_finding = existing_findings[0] + existing_finding.events.append(segmentation_event) + if need_to_overwrite_status(existing_finding.status, status): + existing_finding.status = status + existing_finding.save() diff --git a/monkey/monkey_island/cc/models/zero_trust/test_aggregate_finding.py b/monkey/monkey_island/cc/models/zero_trust/test_aggregate_finding.py new file mode 100644 index 000000000..c1a94166f --- /dev/null +++ b/monkey/monkey_island/cc/models/zero_trust/test_aggregate_finding.py @@ -0,0 +1,53 @@ +from common.data.zero_trust_consts import * +from monkey_island.cc.models.zero_trust.aggregate_finding import AggregateFinding +from monkey_island.cc.models.zero_trust.event import Event +from monkey_island.cc.models.zero_trust.finding import Finding +from monkey_island.cc.testing.IslandTestCase import IslandTestCase + + +class TestAggregateFinding(IslandTestCase): + def test_create_or_add_to_existing(self): + self.fail_if_not_testing_env() + self.clean_finding_db() + + test = TEST_MALICIOUS_ACTIVITY_TIMELINE + status = STATUS_VERIFY + events = [Event.create_event("t", "t", EVENT_TYPE_MONKEY_NETWORK)] + self.assertEquals(len(Finding.objects(test=test, status=status)), 0) + + AggregateFinding.create_or_add_to_existing(test, status, events) + + self.assertEquals(len(Finding.objects(test=test, status=status)), 1) + self.assertEquals(len(Finding.objects(test=test, status=status)[0].events), 1) + + AggregateFinding.create_or_add_to_existing(test, status, events) + + self.assertEquals(len(Finding.objects(test=test, status=status)), 1) + self.assertEquals(len(Finding.objects(test=test, status=status)[0].events), 2) + + def test_create_or_add_to_existing_2_tests_already_exist(self): + self.fail_if_not_testing_env() + self.clean_finding_db() + + test = TEST_MALICIOUS_ACTIVITY_TIMELINE + status = STATUS_VERIFY + event = Event.create_event("t", "t", EVENT_TYPE_MONKEY_NETWORK) + events = [event] + self.assertEquals(len(Finding.objects(test=test, status=status)), 0) + + Finding.save_finding(test, status, events) + + self.assertEquals(len(Finding.objects(test=test, status=status)), 1) + self.assertEquals(len(Finding.objects(test=test, status=status)[0].events), 1) + + AggregateFinding.create_or_add_to_existing(test, status, events) + + self.assertEquals(len(Finding.objects(test=test, status=status)), 1) + self.assertEquals(len(Finding.objects(test=test, status=status)[0].events), 2) + + Finding.save_finding(test, status, events) + + self.assertEquals(len(Finding.objects(test=test, status=status)), 2) + + with self.assertRaises(AssertionError): + AggregateFinding.create_or_add_to_existing(test, status, events) diff --git a/monkey/monkey_island/cc/models/zero_trust/test_event.py b/monkey/monkey_island/cc/models/zero_trust/test_event.py new file mode 100644 index 000000000..c0742407d --- /dev/null +++ b/monkey/monkey_island/cc/models/zero_trust/test_event.py @@ -0,0 +1,32 @@ +from mongoengine import ValidationError + +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 + + +class TestEvent(IslandTestCase): + def test_create_event(self): + self.fail_if_not_testing_env() + self.clean_finding_db() + + with self.assertRaises(ValidationError): + _ = Event.create_event( + title=None, # title required + message="bla bla", + event_type=EVENT_TYPE_MONKEY_NETWORK + ) + + with self.assertRaises(ValidationError): + _ = Event.create_event( + title="skjs", + message="bla bla", + event_type="Unknown" # Unknown event type + ) + + # Assert that nothing is raised. + _ = Event.create_event( + title="skjs", + message="bla bla", + event_type=EVENT_TYPE_MONKEY_NETWORK + ) diff --git a/monkey/monkey_island/cc/models/zero_trust/test_finding.py b/monkey/monkey_island/cc/models/zero_trust/test_finding.py new file mode 100644 index 000000000..88a33d5d3 --- /dev/null +++ b/monkey/monkey_island/cc/models/zero_trust/test_finding.py @@ -0,0 +1,38 @@ +from mongoengine import ValidationError + +from common.data.zero_trust_consts import * +from monkey_island.cc.models.zero_trust.finding import Finding +from monkey_island.cc.models.zero_trust.event import Event +from monkey_island.cc.testing.IslandTestCase import IslandTestCase + + +class TestFinding(IslandTestCase): + """ + Make sure to set server environment to `testing` in server.json! Otherwise this will mess up your mongo instance and + won't work. + + Also, the working directory needs to be the working directory from which you usually run the island so the + server.json file is found and loaded. + """ + def test_save_finding_validation(self): + self.fail_if_not_testing_env() + self.clean_finding_db() + + with self.assertRaises(ValidationError): + _ = Finding.save_finding(test="bla bla", status=STATUS_FAILED, events=[]) + + with self.assertRaises(ValidationError): + _ = Finding.save_finding(test=TEST_SEGMENTATION, status="bla bla", events=[]) + + def test_save_finding_sanity(self): + self.fail_if_not_testing_env() + self.clean_finding_db() + + self.assertEquals(len(Finding.objects(test=TEST_SEGMENTATION)), 0) + + event_example = Event.create_event( + title="Event Title", message="event message", event_type=EVENT_TYPE_MONKEY_NETWORK) + Finding.save_finding(test=TEST_SEGMENTATION, status=STATUS_FAILED, events=[event_example]) + + self.assertEquals(len(Finding.objects(test=TEST_SEGMENTATION)), 1) + self.assertEquals(len(Finding.objects(status=STATUS_FAILED)), 1) diff --git a/monkey/monkey_island/cc/models/zero_trust/test_segmentation_finding.py b/monkey/monkey_island/cc/models/zero_trust/test_segmentation_finding.py new file mode 100644 index 000000000..80e564a17 --- /dev/null +++ b/monkey/monkey_island/cc/models/zero_trust/test_segmentation_finding.py @@ -0,0 +1,52 @@ +from common.data.zero_trust_consts import STATUS_FAILED, EVENT_TYPE_MONKEY_NETWORK +from monkey_island.cc.models.zero_trust.event import Event +from monkey_island.cc.testing.IslandTestCase import IslandTestCase +from monkey_island.cc.models.zero_trust.segmentation_finding import SegmentationFinding + + +class TestSegmentationFinding(IslandTestCase): + def test_create_or_add_to_existing_finding(self): + self.fail_if_not_testing_env() + self.clean_finding_db() + + first_segment = "1.1.1.0/24" + second_segment = "2.2.2.0-2.2.2.254" + third_segment = "3.3.3.3" + event = Event.create_event("bla", "bla", EVENT_TYPE_MONKEY_NETWORK) + + SegmentationFinding.create_or_add_to_existing_finding( + subnets=[first_segment, second_segment], + status=STATUS_FAILED, + segmentation_event=event + ) + + self.assertEquals(len(SegmentationFinding.objects()), 1) + self.assertEquals(len(SegmentationFinding.objects()[0].events), 1) + + SegmentationFinding.create_or_add_to_existing_finding( + # !!! REVERSE ORDER + subnets=[second_segment, first_segment], + status=STATUS_FAILED, + segmentation_event=event + ) + + self.assertEquals(len(SegmentationFinding.objects()), 1) + self.assertEquals(len(SegmentationFinding.objects()[0].events), 2) + + SegmentationFinding.create_or_add_to_existing_finding( + # !!! REVERSE ORDER + subnets=[first_segment, third_segment], + status=STATUS_FAILED, + segmentation_event=event + ) + + self.assertEquals(len(SegmentationFinding.objects()), 2) + + SegmentationFinding.create_or_add_to_existing_finding( + # !!! REVERSE ORDER + subnets=[second_segment, third_segment], + status=STATUS_FAILED, + segmentation_event=event + ) + + self.assertEquals(len(SegmentationFinding.objects()), 3) diff --git a/monkey/monkey_island/cc/resources/report.py b/monkey/monkey_island/cc/resources/report.py deleted file mode 100644 index 62a014fef..000000000 --- a/monkey/monkey_island/cc/resources/report.py +++ /dev/null @@ -1,13 +0,0 @@ -import flask_restful - -from monkey_island.cc.auth import jwt_required -from monkey_island.cc.services.report import ReportService - -__author__ = "itay.mizeretz" - - -class Report(flask_restful.Resource): - - @jwt_required() - def get(self): - return ReportService.get_report() diff --git a/monkey/monkey_island/cc/resources/reporting/__init__.py b/monkey/monkey_island/cc/resources/reporting/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/monkey/monkey_island/cc/resources/reporting/report.py b/monkey/monkey_island/cc/resources/reporting/report.py new file mode 100644 index 000000000..8c5286fee --- /dev/null +++ b/monkey/monkey_island/cc/resources/reporting/report.py @@ -0,0 +1,41 @@ +import httplib + + +import flask_restful +from flask import jsonify + +from monkey_island.cc.auth import jwt_required +from monkey_island.cc.services.reporting.report import ReportService +from monkey_island.cc.services.reporting.zero_trust_service import ZeroTrustService + +ZERO_TRUST_REPORT_TYPE = "zero_trust" +SECURITY_REPORT_TYPE = "security" +REPORT_TYPES = [SECURITY_REPORT_TYPE, ZERO_TRUST_REPORT_TYPE] + +REPORT_DATA_PILLARS = "pillars" +REPORT_DATA_FINDINGS = "findings" +REPORT_DATA_PRINCIPLES_STATUS = "principles" + +__author__ = ["itay.mizeretz", "shay.nehmad"] + + +class Report(flask_restful.Resource): + + @jwt_required() + def get(self, report_type=SECURITY_REPORT_TYPE, report_data=None): + if report_type == SECURITY_REPORT_TYPE: + return ReportService.get_report() + elif report_type == ZERO_TRUST_REPORT_TYPE: + if report_data == REPORT_DATA_PILLARS: + return jsonify({ + "statusesToPillars": ZeroTrustService.get_statuses_to_pillars(), + "pillarsToStatuses": ZeroTrustService.get_pillars_to_statuses(), + "grades": ZeroTrustService.get_pillars_grades() + } + ) + elif report_data == REPORT_DATA_PRINCIPLES_STATUS: + return jsonify(ZeroTrustService.get_principles_status()) + elif report_data == REPORT_DATA_FINDINGS: + return jsonify(ZeroTrustService.get_all_findings()) + + flask_restful.abort(httplib.NOT_FOUND) diff --git a/monkey/monkey_island/cc/resources/root.py b/monkey/monkey_island/cc/resources/root.py index 2af73a45e..e3b3e9854 100644 --- a/monkey/monkey_island/cc/resources/root.py +++ b/monkey/monkey_island/cc/resources/root.py @@ -7,7 +7,7 @@ from flask import request, make_response, jsonify 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.reporting.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 diff --git a/monkey/monkey_island/cc/resources/telemetry.py b/monkey/monkey_island/cc/resources/telemetry.py index fa942d174..dc6a7d512 100644 --- a/monkey/monkey_island/cc/resources/telemetry.py +++ b/monkey/monkey_island/cc/resources/telemetry.py @@ -1,6 +1,5 @@ import json import logging -import copy from datetime import datetime import dateutil @@ -9,12 +8,8 @@ from flask import request from monkey_island.cc.auth import jwt_required from monkey_island.cc.database import mongo -from monkey_island.cc.services import mimikatz_utils -from monkey_island.cc.services.config import ConfigService -from monkey_island.cc.services.edge import EdgeService from monkey_island.cc.services.node import NodeService -from monkey_island.cc.encryptor import encryptor -from monkey_island.cc.services.wmi_handler import WMIHandler +from monkey_island.cc.services.telemetry.processing.processing import process_telemetry from monkey_island.cc.models.monkey import Monkey __author__ = 'Barak' @@ -54,16 +49,9 @@ class Telemetry(flask_restful.Resource): Monkey.get_single_monkey_by_guid(telemetry_json['monkey_guid']).renew_ttl() monkey = NodeService.get_monkey_by_guid(telemetry_json['monkey_guid']) + NodeService.update_monkey_modify_time(monkey["_id"]) - try: - NodeService.update_monkey_modify_time(monkey["_id"]) - telem_category = telemetry_json.get('telem_category') - if telem_category in TELEM_PROCESS_DICT: - TELEM_PROCESS_DICT[telem_category](telemetry_json) - else: - logger.info('Got unknown type of telemetry: %s' % telem_category) - except Exception as ex: - logger.error("Exception caught while processing telemetry. Info: {}".format(ex.message), exc_info=True) + process_telemetry(telemetry_json) telem_id = mongo.db.telemetry.insert(telemetry_json) return mongo.db.telemetry.find_one_or_404({"_id": telem_id}) @@ -90,200 +78,3 @@ class Telemetry(flask_restful.Resource): x['data']['credentials'][new_user] = x['data']['credentials'].pop(user) return objects - - @staticmethod - def get_edge_by_scan_or_exploit_telemetry(telemetry_json): - dst_ip = telemetry_json['data']['machine']['ip_addr'] - dst_domain_name = telemetry_json['data']['machine']['domain_name'] - src_monkey = NodeService.get_monkey_by_guid(telemetry_json['monkey_guid']) - dst_node = NodeService.get_monkey_by_ip(dst_ip) - if dst_node is None: - dst_node = NodeService.get_or_create_node(dst_ip, dst_domain_name) - - return EdgeService.get_or_create_edge(src_monkey["_id"], dst_node["_id"]) - - @staticmethod - def process_tunnel_telemetry(telemetry_json): - monkey_id = NodeService.get_monkey_by_guid(telemetry_json['monkey_guid'])["_id"] - if telemetry_json['data']['proxy'] is not None: - tunnel_host_ip = telemetry_json['data']['proxy'].split(":")[-2].replace("//", "") - NodeService.set_monkey_tunnel(monkey_id, tunnel_host_ip) - else: - NodeService.unset_all_monkey_tunnels(monkey_id) - - @staticmethod - def process_state_telemetry(telemetry_json): - monkey = NodeService.get_monkey_by_guid(telemetry_json['monkey_guid']) - NodeService.add_communication_info(monkey, telemetry_json['command_control_channel']) - if telemetry_json['data']['done']: - NodeService.set_monkey_dead(monkey, True) - else: - NodeService.set_monkey_dead(monkey, False) - - @staticmethod - def process_exploit_telemetry(telemetry_json): - edge = Telemetry.get_edge_by_scan_or_exploit_telemetry(telemetry_json) - Telemetry.encrypt_exploit_creds(telemetry_json) - telemetry_json['data']['info']['started'] = dateutil.parser.parse(telemetry_json['data']['info']['started']) - telemetry_json['data']['info']['finished'] = dateutil.parser.parse(telemetry_json['data']['info']['finished']) - - new_exploit = copy.deepcopy(telemetry_json['data']) - - new_exploit.pop('machine') - new_exploit['timestamp'] = telemetry_json['timestamp'] - - mongo.db.edge.update( - {'_id': edge['_id']}, - {'$push': {'exploits': new_exploit}} - ) - if new_exploit['result']: - EdgeService.set_edge_exploited(edge) - - for attempt in telemetry_json['data']['attempts']: - if attempt['result']: - found_creds = {'user': attempt['user']} - for field in ['password', 'lm_hash', 'ntlm_hash', 'ssh_key']: - if len(attempt[field]) != 0: - found_creds[field] = attempt[field] - NodeService.add_credentials_to_node(edge['to'], found_creds) - - @staticmethod - def process_scan_telemetry(telemetry_json): - edge = Telemetry.get_edge_by_scan_or_exploit_telemetry(telemetry_json) - data = copy.deepcopy(telemetry_json['data']['machine']) - ip_address = data.pop("ip_addr") - domain_name = data.pop("domain_name") - new_scan = \ - { - "timestamp": telemetry_json["timestamp"], - "data": data - } - mongo.db.edge.update( - {"_id": edge["_id"]}, - {"$push": {"scans": new_scan}, - "$set": {"ip_address": ip_address, 'domain_name': domain_name}} - ) - - node = mongo.db.node.find_one({"_id": edge["to"]}) - if node is not None: - scan_os = new_scan["data"]["os"] - if "type" in scan_os: - mongo.db.node.update({"_id": node["_id"]}, - {"$set": {"os.type": scan_os["type"]}}, - upsert=False) - if "version" in scan_os: - mongo.db.node.update({"_id": node["_id"]}, - {"$set": {"os.version": scan_os["version"]}}, - upsert=False) - - @staticmethod - def process_system_info_telemetry(telemetry_json): - users_secrets = {} - monkey_id = NodeService.get_monkey_by_guid(telemetry_json['monkey_guid']).get('_id') - if 'ssh_info' in telemetry_json['data']: - ssh_info = telemetry_json['data']['ssh_info'] - Telemetry.encrypt_system_info_ssh_keys(ssh_info) - if telemetry_json['data']['network_info']['networks']: - # We use user_name@machine_ip as the name of the ssh key stolen, thats why we need ip from telemetry - Telemetry.add_ip_to_ssh_keys(telemetry_json['data']['network_info']['networks'][0], ssh_info) - Telemetry.add_system_info_ssh_keys_to_config(ssh_info) - if 'credentials' in telemetry_json['data']: - creds = telemetry_json['data']['credentials'] - Telemetry.encrypt_system_info_creds(creds) - Telemetry.add_system_info_creds_to_config(creds) - Telemetry.replace_user_dot_with_comma(creds) - if 'mimikatz' in telemetry_json['data']: - users_secrets = mimikatz_utils.MimikatzSecrets. \ - extract_secrets_from_mimikatz(telemetry_json['data'].get('mimikatz', '')) - if 'wmi' in telemetry_json['data']: - wmi_handler = WMIHandler(monkey_id, telemetry_json['data']['wmi'], users_secrets) - wmi_handler.process_and_handle_wmi_info() - if 'aws' in telemetry_json['data']: - if 'instance_id' in telemetry_json['data']['aws']: - mongo.db.monkey.update_one({'_id': monkey_id}, - {'$set': {'aws_instance_id': telemetry_json['data']['aws']['instance_id']}}) - - @staticmethod - def add_ip_to_ssh_keys(ip, ssh_info): - for key in ssh_info: - key['ip'] = ip['addr'] - - @staticmethod - def process_trace_telemetry(telemetry_json): - # Nothing to do - return - - @staticmethod - def replace_user_dot_with_comma(creds): - for user in creds: - if -1 != user.find('.'): - new_user = user.replace('.', ',') - creds[new_user] = creds.pop(user) - - @staticmethod - def encrypt_system_info_creds(creds): - for user in creds: - for field in ['password', 'lm_hash', 'ntlm_hash']: - if field in creds[user]: - # this encoding is because we might run into passwords which are not pure ASCII - creds[user][field] = encryptor.enc(creds[user][field].encode('utf-8')) - - @staticmethod - def encrypt_system_info_ssh_keys(ssh_info): - for idx, user in enumerate(ssh_info): - for field in ['public_key', 'private_key', 'known_hosts']: - if ssh_info[idx][field]: - ssh_info[idx][field] = encryptor.enc(ssh_info[idx][field].encode('utf-8')) - - @staticmethod - def add_system_info_creds_to_config(creds): - for user in creds: - ConfigService.creds_add_username(user) - if 'password' in creds[user]: - ConfigService.creds_add_password(creds[user]['password']) - if 'lm_hash' in creds[user]: - ConfigService.creds_add_lm_hash(creds[user]['lm_hash']) - if 'ntlm_hash' in creds[user]: - ConfigService.creds_add_ntlm_hash(creds[user]['ntlm_hash']) - - @staticmethod - def add_system_info_ssh_keys_to_config(ssh_info): - for user in ssh_info: - ConfigService.creds_add_username(user['name']) - # Public key is useless without private key - if user['public_key'] and user['private_key']: - ConfigService.ssh_add_keys(user['public_key'], user['private_key'], - user['name'], user['ip']) - - @staticmethod - def encrypt_exploit_creds(telemetry_json): - attempts = telemetry_json['data']['attempts'] - for i in range(len(attempts)): - for field in ['password', 'lm_hash', 'ntlm_hash']: - credential = attempts[i][field] - if len(credential) > 0: - attempts[i][field] = encryptor.enc(credential.encode('utf-8')) - - @staticmethod - def process_post_breach_telemetry(telemetry_json): - mongo.db.monkey.update( - {'guid': telemetry_json['monkey_guid']}, - {'$push': {'pba_results': telemetry_json['data']}}) - - @staticmethod - def process_attack_telemetry(telemetry_json): - # No processing required - pass - - -TELEM_PROCESS_DICT = \ - { - 'tunnel': Telemetry.process_tunnel_telemetry, - 'state': Telemetry.process_state_telemetry, - 'exploit': Telemetry.process_exploit_telemetry, - 'scan': Telemetry.process_scan_telemetry, - 'system_info': Telemetry.process_system_info_telemetry, - 'trace': Telemetry.process_trace_telemetry, - 'post_breach': Telemetry.process_post_breach_telemetry, - 'attack': Telemetry.process_attack_telemetry - } diff --git a/monkey/monkey_island/cc/services/config_schema.py b/monkey/monkey_island/cc/services/config_schema.py index 4ef418b6c..f3a6c9fac 100644 --- a/monkey/monkey_island/cc/services/config_schema.py +++ b/monkey/monkey_island/cc/services/config_schema.py @@ -111,6 +111,14 @@ SCHEMA = { "title": "Back door user", "attack_techniques": [] }, + { + "type": "string", + "enum": [ + "CommunicateAsNewUser" + ], + "title": "Communicate as new user", + "attack_techniques": [] + }, ], }, "finger_classes": { @@ -329,6 +337,7 @@ SCHEMA = { "$ref": "#/definitions/post_breach_acts" }, "default": [ + "CommunicateAsNewUser" ], "description": "List of actions the Monkey will run post breach" }, diff --git a/monkey/monkey_island/cc/services/configuration/__init__.py b/monkey/monkey_island/cc/services/configuration/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/monkey/monkey_island/cc/services/configuration/utils.py b/monkey/monkey_island/cc/services/configuration/utils.py new file mode 100644 index 000000000..34d6a9bb5 --- /dev/null +++ b/monkey/monkey_island/cc/services/configuration/utils.py @@ -0,0 +1,5 @@ +from monkey_island.cc.services.config import ConfigService + + +def get_config_network_segments_as_subnet_groups(): + return [ConfigService.get_config_value(['basic_network', 'network_analysis', 'inaccessible_subnets'])] diff --git a/monkey/monkey_island/cc/services/pth_report.py b/monkey/monkey_island/cc/services/reporting/pth_report.py similarity index 100% rename from monkey/monkey_island/cc/services/pth_report.py rename to monkey/monkey_island/cc/services/reporting/pth_report.py diff --git a/monkey/monkey_island/cc/services/report.py b/monkey/monkey_island/cc/services/reporting/report.py similarity index 96% rename from monkey/monkey_island/cc/services/report.py rename to monkey/monkey_island/cc/services/reporting/report.py index 409586e66..f00fbc22c 100644 --- a/monkey/monkey_island/cc/services/report.py +++ b/monkey/monkey_island/cc/services/reporting/report.py @@ -9,14 +9,16 @@ from enum import Enum from six import text_type +from common.network.segmentation_utils import get_ip_in_src_and_not_in_dst from monkey_island.cc.database import mongo from monkey_island.cc.models import Monkey from monkey_island.cc.services.reporting.report_exporter_manager import ReportExporterManager from monkey_island.cc.services.config import ConfigService +from monkey_island.cc.services.configuration.utils import get_config_network_segments_as_subnet_groups from monkey_island.cc.services.edge import EdgeService from monkey_island.cc.services.node import NodeService from monkey_island.cc.utils import local_ip_addresses, get_subnets -from monkey_island.cc.services.pth_report import PTHReportService +from monkey_island.cc.services.reporting.pth_report import PTHReportService from common.network.network_range import NetworkRange __author__ = "itay.mizeretz" @@ -415,23 +417,6 @@ class ReportService: return issues - @staticmethod - def get_ip_in_src_and_not_in_dst(ip_addresses, source_subnet, target_subnet): - """ - Finds an IP address in ip_addresses which is in source_subnet but not in target_subnet. - :param ip_addresses: List of IP addresses to test. - :param source_subnet: Subnet to want an IP to not be in. - :param target_subnet: Subnet we want an IP to be in. - :return: - """ - for ip_address in ip_addresses: - if target_subnet.is_in_range(ip_address): - return None - for ip_address in ip_addresses: - if source_subnet.is_in_range(ip_address): - return ip_address - return None - @staticmethod def get_cross_segment_issues_of_single_machine(source_subnet_range, target_subnet_range): """ @@ -494,9 +479,9 @@ class ReportService: target_ip = scan['data']['machine']['ip_addr'] if target_subnet_range.is_in_range(text_type(target_ip)): monkey = NodeService.get_monkey_by_guid(scan['monkey_guid']) - cross_segment_ip = ReportService.get_ip_in_src_and_not_in_dst(monkey['ip_addresses'], - source_subnet_range, - target_subnet_range) + cross_segment_ip = get_ip_in_src_and_not_in_dst(monkey['ip_addresses'], + source_subnet_range, + target_subnet_range) if cross_segment_ip is not None: cross_segment_issues.append( @@ -544,7 +529,7 @@ class ReportService: cross_segment_issues = [] # For now the feature is limited to 1 group. - subnet_groups = [ConfigService.get_config_value(['basic_network', 'network_analysis', 'inaccessible_subnets'])] + subnet_groups = get_config_network_segments_as_subnet_groups() for subnet_group in subnet_groups: cross_segment_issues += ReportService.get_cross_segment_issues_per_subnet_group(scans, subnet_group) diff --git a/monkey/monkey_island/cc/services/pth_report_test.py b/monkey/monkey_island/cc/services/reporting/test_pth_report.py similarity index 96% rename from monkey/monkey_island/cc/services/pth_report_test.py rename to monkey/monkey_island/cc/services/reporting/test_pth_report.py index 24e560ff0..f934f50ab 100644 --- a/monkey/monkey_island/cc/services/pth_report_test.py +++ b/monkey/monkey_island/cc/services/reporting/test_pth_report.py @@ -1,7 +1,7 @@ import uuid from monkey_island.cc.models import Monkey -from monkey_island.cc.services.pth_report import PTHReportService +from monkey_island.cc.services.reporting.pth_report import PTHReportService from monkey_island.cc.testing.IslandTestCase import IslandTestCase diff --git a/monkey/monkey_island/cc/services/reporting/test_zero_trust_service.py b/monkey/monkey_island/cc/services/reporting/test_zero_trust_service.py new file mode 100644 index 000000000..46b4fefd7 --- /dev/null +++ b/monkey/monkey_island/cc/services/reporting/test_zero_trust_service.py @@ -0,0 +1,285 @@ +from monkey_island.cc.services.reporting.zero_trust_service import ZeroTrustService + +from common.data.zero_trust_consts import * +from monkey_island.cc.models.zero_trust.finding import Finding +from monkey_island.cc.testing.IslandTestCase import IslandTestCase + + +def save_example_findings(): + # arrange + Finding.save_finding(TEST_ENDPOINT_SECURITY_EXISTS, STATUS_PASSED, []) # devices passed = 1 + Finding.save_finding(TEST_ENDPOINT_SECURITY_EXISTS, STATUS_PASSED, []) # devices passed = 2 + Finding.save_finding(TEST_ENDPOINT_SECURITY_EXISTS, STATUS_FAILED, []) # devices failed = 1 + # devices unexecuted = 1 + # people verify = 1 + # networks verify = 1 + Finding.save_finding(TEST_SCHEDULED_EXECUTION, STATUS_VERIFY, []) + # people verify = 2 + # networks verify = 2 + Finding.save_finding(TEST_SCHEDULED_EXECUTION, STATUS_VERIFY, []) + # data failed 1 + Finding.save_finding(TEST_DATA_ENDPOINT_HTTP, STATUS_FAILED, []) + # data failed 2 + Finding.save_finding(TEST_DATA_ENDPOINT_HTTP, STATUS_FAILED, []) + # data failed 3 + Finding.save_finding(TEST_DATA_ENDPOINT_HTTP, STATUS_FAILED, []) + # data failed 4 + Finding.save_finding(TEST_DATA_ENDPOINT_HTTP, STATUS_FAILED, []) + # data failed 5 + Finding.save_finding(TEST_DATA_ENDPOINT_HTTP, STATUS_FAILED, []) + # data verify 1 + Finding.save_finding(TEST_DATA_ENDPOINT_HTTP, STATUS_VERIFY, []) + # data verify 2 + Finding.save_finding(TEST_DATA_ENDPOINT_HTTP, STATUS_VERIFY, []) + # data passed 1 + Finding.save_finding(TEST_DATA_ENDPOINT_HTTP, STATUS_PASSED, []) + + +class TestZeroTrustService(IslandTestCase): + def test_get_pillars_grades(self): + self.fail_if_not_testing_env() + self.clean_finding_db() + + save_example_findings() + + expected = [ + { + STATUS_FAILED: 5, + STATUS_VERIFY: 2, + STATUS_PASSED: 1, + STATUS_UNEXECUTED: 1, + "pillar": "Data" + }, + { + STATUS_FAILED: 0, + STATUS_VERIFY: 2, + STATUS_PASSED: 0, + STATUS_UNEXECUTED: 1, + "pillar": "People" + }, + { + STATUS_FAILED: 0, + STATUS_VERIFY: 2, + STATUS_PASSED: 0, + STATUS_UNEXECUTED: 4, + "pillar": "Networks" + }, + { + STATUS_FAILED: 1, + STATUS_VERIFY: 0, + STATUS_PASSED: 2, + STATUS_UNEXECUTED: 1, + "pillar": "Devices" + }, + { + STATUS_FAILED: 0, + STATUS_VERIFY: 0, + STATUS_PASSED: 0, + STATUS_UNEXECUTED: 0, + "pillar": "Workloads" + }, + { + STATUS_FAILED: 0, + STATUS_VERIFY: 0, + STATUS_PASSED: 0, + STATUS_UNEXECUTED: 3, + "pillar": "Visibility & Analytics" + }, + { + STATUS_FAILED: 0, + STATUS_VERIFY: 0, + STATUS_PASSED: 0, + STATUS_UNEXECUTED: 0, + "pillar": "Automation & Orchestration" + } + ] + + result = ZeroTrustService.get_pillars_grades() + + self.assertEquals(result, expected) + + def test_get_principles_status(self): + self.fail_if_not_testing_env() + self.clean_finding_db() + + self.maxDiff = None + + save_example_findings() + + expected = { + AUTOMATION_ORCHESTRATION: [], + DATA: [ + { + "principle": PRINCIPLES[PRINCIPLE_DATA_TRANSIT], + "status": STATUS_FAILED, + "tests": [ + { + "status": STATUS_FAILED, + "test": TESTS_MAP[TEST_DATA_ENDPOINT_HTTP][TEST_EXPLANATION_KEY] + }, + { + "status": STATUS_UNEXECUTED, + "test": TESTS_MAP[TEST_DATA_ENDPOINT_ELASTIC][TEST_EXPLANATION_KEY] + }, + ] + } + ], + DEVICES: [ + { + "principle": PRINCIPLES[PRINCIPLE_ENDPOINT_SECURITY], + "status": STATUS_FAILED, + "tests": [ + { + "status": STATUS_UNEXECUTED, + "test": TESTS_MAP[TEST_MACHINE_EXPLOITED][TEST_EXPLANATION_KEY] + }, + { + "status": STATUS_FAILED, + "test": TESTS_MAP[TEST_ENDPOINT_SECURITY_EXISTS][TEST_EXPLANATION_KEY] + }, + ] + } + ], + NETWORKS: [ + { + "principle": PRINCIPLES[PRINCIPLE_SEGMENTATION], + "status": STATUS_UNEXECUTED, + "tests": [ + { + "status": STATUS_UNEXECUTED, + "test": TESTS_MAP[TEST_SEGMENTATION][TEST_EXPLANATION_KEY] + } + ] + }, + { + "principle": PRINCIPLES[PRINCIPLE_USER_BEHAVIOUR], + "status": STATUS_VERIFY, + "tests": [ + { + "status": STATUS_VERIFY, + "test": TESTS_MAP[TEST_SCHEDULED_EXECUTION][TEST_EXPLANATION_KEY] + } + ] + }, + { + "principle": PRINCIPLES[PRINCIPLE_USERS_MAC_POLICIES], + "status": STATUS_UNEXECUTED, + "tests": [ + { + "status": STATUS_UNEXECUTED, + "test": TESTS_MAP[TEST_COMMUNICATE_AS_NEW_USER][TEST_EXPLANATION_KEY] + } + ] + }, + { + "principle": PRINCIPLES[PRINCIPLE_ANALYZE_NETWORK_TRAFFIC], + "status": STATUS_UNEXECUTED, + "tests": [ + { + "status": STATUS_UNEXECUTED, + "test": TESTS_MAP[TEST_MALICIOUS_ACTIVITY_TIMELINE][TEST_EXPLANATION_KEY] + } + ] + }, + { + "principle": PRINCIPLES[PRINCIPLE_RESTRICTIVE_NETWORK_POLICIES], + "status": STATUS_UNEXECUTED, + "tests": [ + { + "status": STATUS_UNEXECUTED, + "test": TESTS_MAP[TEST_TUNNELING][TEST_EXPLANATION_KEY] + } + ] + }, + ], + PEOPLE: [ + { + "principle": PRINCIPLES[PRINCIPLE_USER_BEHAVIOUR], + "status": STATUS_VERIFY, + "tests": [ + { + "status": STATUS_VERIFY, + "test": TESTS_MAP[TEST_SCHEDULED_EXECUTION][TEST_EXPLANATION_KEY] + } + ] + }, + { + "principle": PRINCIPLES[PRINCIPLE_USERS_MAC_POLICIES], + "status": STATUS_UNEXECUTED, + "tests": [ + { + "status": STATUS_UNEXECUTED, + "test": TESTS_MAP[TEST_COMMUNICATE_AS_NEW_USER][TEST_EXPLANATION_KEY] + } + ] + } + ], + VISIBILITY_ANALYTICS: [ + { + "principle": PRINCIPLES[PRINCIPLE_USERS_MAC_POLICIES], + "status": STATUS_UNEXECUTED, + "tests": [ + { + "status": STATUS_UNEXECUTED, + "test": TESTS_MAP[TEST_COMMUNICATE_AS_NEW_USER][TEST_EXPLANATION_KEY] + } + ] + }, + { + "principle": PRINCIPLES[PRINCIPLE_ANALYZE_NETWORK_TRAFFIC], + "status": STATUS_UNEXECUTED, + "tests": [ + { + "status": STATUS_UNEXECUTED, + "test": TESTS_MAP[TEST_MALICIOUS_ACTIVITY_TIMELINE][TEST_EXPLANATION_KEY] + } + ] + }, + { + "principle": PRINCIPLES[PRINCIPLE_RESTRICTIVE_NETWORK_POLICIES], + "status": STATUS_UNEXECUTED, + "tests": [ + { + "status": STATUS_UNEXECUTED, + "test": TESTS_MAP[TEST_TUNNELING][TEST_EXPLANATION_KEY] + } + ] + }, + ], + WORKLOADS: [] + } + + result = ZeroTrustService.get_principles_status() + self.assertEquals(result, expected) + + def test_get_pillars_to_statuses(self): + self.fail_if_not_testing_env() + self.clean_finding_db() + + self.maxDiff = None + + expected = { + AUTOMATION_ORCHESTRATION: STATUS_UNEXECUTED, + DEVICES: STATUS_UNEXECUTED, + NETWORKS: STATUS_UNEXECUTED, + PEOPLE: STATUS_UNEXECUTED, + VISIBILITY_ANALYTICS: STATUS_UNEXECUTED, + WORKLOADS: STATUS_UNEXECUTED, + DATA: STATUS_UNEXECUTED + } + + self.assertEquals(ZeroTrustService.get_pillars_to_statuses(), expected) + + save_example_findings() + + expected = { + AUTOMATION_ORCHESTRATION: STATUS_UNEXECUTED, + DEVICES: STATUS_FAILED, + NETWORKS: STATUS_VERIFY, + PEOPLE: STATUS_VERIFY, + VISIBILITY_ANALYTICS: STATUS_UNEXECUTED, + WORKLOADS: STATUS_UNEXECUTED, + DATA: STATUS_FAILED + } + + self.assertEquals(ZeroTrustService.get_pillars_to_statuses(), expected) diff --git a/monkey/monkey_island/cc/services/reporting/zero_trust_service.py b/monkey/monkey_island/cc/services/reporting/zero_trust_service.py new file mode 100644 index 000000000..f4b23f095 --- /dev/null +++ b/monkey/monkey_island/cc/services/reporting/zero_trust_service.py @@ -0,0 +1,150 @@ +import json +from common.data.zero_trust_consts import * +from monkey_island.cc.models.zero_trust.finding import Finding + + +class ZeroTrustService(object): + @staticmethod + def get_pillars_grades(): + pillars_grades = [] + for pillar in PILLARS: + pillars_grades.append(ZeroTrustService.__get_pillar_grade(pillar)) + return pillars_grades + + @staticmethod + def __get_pillar_grade(pillar): + all_findings = Finding.objects() + pillar_grade = { + "pillar": pillar, + STATUS_FAILED: 0, + STATUS_VERIFY: 0, + STATUS_PASSED: 0, + STATUS_UNEXECUTED: 0 + } + + tests_of_this_pillar = PILLARS_TO_TESTS[pillar] + + test_unexecuted = {} + for test in tests_of_this_pillar: + test_unexecuted[test] = True + + for finding in all_findings: + test_unexecuted[finding.test] = False + test_info = TESTS_MAP[finding.test] + if pillar in test_info[PILLARS_KEY]: + pillar_grade[finding.status] += 1 + + pillar_grade[STATUS_UNEXECUTED] = sum(1 for condition in test_unexecuted.values() if condition) + + return pillar_grade + + @staticmethod + def get_principles_status(): + all_principles_statuses = {} + + # init with empty lists + for pillar in PILLARS: + all_principles_statuses[pillar] = [] + + for principle, principle_tests in PRINCIPLES_TO_TESTS.items(): + for pillar in PRINCIPLES_TO_PILLARS[principle]: + all_principles_statuses[pillar].append( + { + "principle": PRINCIPLES[principle], + "tests": ZeroTrustService.__get_tests_status(principle_tests), + "status": ZeroTrustService.__get_principle_status(principle_tests) + } + ) + + return all_principles_statuses + + @staticmethod + def __get_principle_status(principle_tests): + worst_status = STATUS_UNEXECUTED + all_statuses = set() + for test in principle_tests: + all_statuses |= set(Finding.objects(test=test).distinct("status")) + + for status in all_statuses: + if ORDERED_TEST_STATUSES.index(status) < ORDERED_TEST_STATUSES.index(worst_status): + worst_status = status + + return worst_status + + @staticmethod + def __get_tests_status(principle_tests): + results = [] + for test in principle_tests: + test_findings = Finding.objects(test=test) + results.append( + { + "test": TESTS_MAP[test][TEST_EXPLANATION_KEY], + "status": ZeroTrustService.__get_lcd_worst_status_for_test(test_findings) + } + ) + return results + + @staticmethod + def __get_lcd_worst_status_for_test(all_findings_for_test): + """ + :param all_findings_for_test: All findings of a specific test (get this using Finding.objects(test={A_TEST})) + :return: the "worst" (i.e. most severe) status out of the given findings. + lcd stands for lowest common denominator. + """ + current_worst_status = STATUS_UNEXECUTED + for finding in all_findings_for_test: + if ORDERED_TEST_STATUSES.index(finding.status) < ORDERED_TEST_STATUSES.index(current_worst_status): + current_worst_status = finding.status + + return current_worst_status + + @staticmethod + def get_all_findings(): + all_findings = Finding.objects() + enriched_findings = [ZeroTrustService.__get_enriched_finding(f) for f in all_findings] + return enriched_findings + + @staticmethod + def __get_enriched_finding(finding): + test_info = TESTS_MAP[finding.test] + enriched_finding = { + "test": test_info[FINDING_EXPLANATION_BY_STATUS_KEY][finding.status], + "test_key": finding.test, + "pillars": test_info[PILLARS_KEY], + "status": finding.status, + "events": ZeroTrustService.__get_events_as_dict(finding.events) + } + return enriched_finding + + @staticmethod + def __get_events_as_dict(events): + return [json.loads(event.to_json()) for event in events] + + @staticmethod + def get_statuses_to_pillars(): + results = { + STATUS_FAILED: [], + STATUS_VERIFY: [], + STATUS_PASSED: [], + STATUS_UNEXECUTED: [] + } + for pillar in PILLARS: + results[ZeroTrustService.__get_status_of_single_pillar(pillar)].append(pillar) + + return results + + @staticmethod + def get_pillars_to_statuses(): + results = {} + for pillar in PILLARS: + results[pillar] = ZeroTrustService.__get_status_of_single_pillar(pillar) + + return results + + @staticmethod + def __get_status_of_single_pillar(pillar): + grade = ZeroTrustService.__get_pillar_grade(pillar) + for status in ORDERED_TEST_STATUSES: + if grade[status] > 0: + return status + return STATUS_UNEXECUTED diff --git a/monkey/monkey_island/cc/services/telemetry/__init__.py b/monkey/monkey_island/cc/services/telemetry/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/monkey/monkey_island/cc/services/telemetry/processing/__init__.py b/monkey/monkey_island/cc/services/telemetry/processing/__init__.py new file mode 100644 index 000000000..d90143c09 --- /dev/null +++ b/monkey/monkey_island/cc/services/telemetry/processing/__init__.py @@ -0,0 +1,7 @@ +# import all implemented hooks, for brevity of hooks.py file +from tunnel import process_tunnel_telemetry +from state import process_state_telemetry +from exploit import process_exploit_telemetry +from scan import process_scan_telemetry +from system_info import process_system_info_telemetry +from post_breach import process_post_breach_telemetry diff --git a/monkey/monkey_island/cc/services/telemetry/processing/exploit.py b/monkey/monkey_island/cc/services/telemetry/processing/exploit.py new file mode 100644 index 000000000..cf6e9b544 --- /dev/null +++ b/monkey/monkey_island/cc/services/telemetry/processing/exploit.py @@ -0,0 +1,58 @@ +import copy + +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 +from monkey_island.cc.services.telemetry.zero_trust_tests.machine_exploited import test_machine_exploited + + +def process_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( + 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): + for attempt in telemetry_json['data']['attempts']: + if attempt['result']: + found_creds = {'user': attempt['user']} + for field in ['password', 'lm_hash', 'ntlm_hash', 'ssh_key']: + if len(attempt[field]) != 0: + found_creds[field] = attempt[field] + NodeService.add_credentials_to_node(edge['to'], found_creds) + + +def update_edge_info_with_new_exploit(edge, telemetry_json): + telemetry_json['data']['info']['started'] = dateutil.parser.parse(telemetry_json['data']['info']['started']) + telemetry_json['data']['info']['finished'] = dateutil.parser.parse(telemetry_json['data']['info']['finished']) + new_exploit = copy.deepcopy(telemetry_json['data']) + new_exploit.pop('machine') + new_exploit['timestamp'] = telemetry_json['timestamp'] + mongo.db.edge.update( + {'_id': edge['_id']}, + {'$push': {'exploits': new_exploit}} + ) + if new_exploit['result']: + EdgeService.set_edge_exploited(edge) + + +def encrypt_exploit_creds(telemetry_json): + attempts = telemetry_json['data']['attempts'] + for i in range(len(attempts)): + for field in ['password', 'lm_hash', 'ntlm_hash']: + credential = attempts[i][field] + if len(credential) > 0: + attempts[i][field] = encryptor.enc(credential.encode('utf-8')) diff --git a/monkey/monkey_island/cc/services/telemetry/processing/post_breach.py b/monkey/monkey_island/cc/services/telemetry/processing/post_breach.py new file mode 100644 index 000000000..c64849905 --- /dev/null +++ b/monkey/monkey_island/cc/services/telemetry/processing/post_breach.py @@ -0,0 +1,27 @@ +from monkey_island.cc.database import mongo +from common.data.post_breach_consts import * +from monkey_island.cc.models import Monkey +from monkey_island.cc.services.telemetry.zero_trust_tests.communicate_as_new_user import test_new_user_communication + + +def process_communicate_as_new_user_telemetry(telemetry_json): + current_monkey = Monkey.get_single_monkey_by_guid(telemetry_json['monkey_guid']) + message = telemetry_json['data']['result'][0] + success = telemetry_json['data']['result'][1] + test_new_user_communication(current_monkey, success, message) + + +POST_BREACH_TELEMETRY_PROCESSING_FUNCS = { + POST_BREACH_COMMUNICATE_AS_NEW_USER: process_communicate_as_new_user_telemetry, +} + + +def process_post_breach_telemetry(telemetry_json): + mongo.db.monkey.update( + {'guid': telemetry_json['monkey_guid']}, + {'$push': {'pba_results': telemetry_json['data']}}) + + post_breach_action_name = telemetry_json["data"]["name"] + if post_breach_action_name in POST_BREACH_TELEMETRY_PROCESSING_FUNCS: + POST_BREACH_TELEMETRY_PROCESSING_FUNCS[post_breach_action_name](telemetry_json) + diff --git a/monkey/monkey_island/cc/services/telemetry/processing/processing.py b/monkey/monkey_island/cc/services/telemetry/processing/processing.py new file mode 100644 index 000000000..154096f79 --- /dev/null +++ b/monkey/monkey_island/cc/services/telemetry/processing/processing.py @@ -0,0 +1,29 @@ +import logging + +from monkey_island.cc.services.telemetry.processing import * + +logger = logging.getLogger(__name__) + +TELEMETRY_CATEGORY_TO_PROCESSING_FUNC = \ + { + 'tunnel': process_tunnel_telemetry, + 'state': process_state_telemetry, + 'exploit': process_exploit_telemetry, + 'scan': process_scan_telemetry, + 'system_info': process_system_info_telemetry, + 'post_breach': process_post_breach_telemetry, + # `lambda *args, **kwargs: None` is a no-op. + 'trace': lambda *args, **kwargs: None, + 'attack': lambda *args, **kwargs: None, + } + + +def process_telemetry(telemetry_json): + try: + telem_category = telemetry_json.get('telem_category') + if telem_category in TELEMETRY_CATEGORY_TO_PROCESSING_FUNC: + TELEMETRY_CATEGORY_TO_PROCESSING_FUNC[telem_category](telemetry_json) + else: + logger.info('Got unknown type of telemetry: %s' % telem_category) + except Exception as ex: + logger.error("Exception caught while processing telemetry. Info: {}".format(ex.message), exc_info=True) diff --git a/monkey/monkey_island/cc/services/telemetry/processing/scan.py b/monkey/monkey_island/cc/services/telemetry/processing/scan.py new file mode 100644 index 000000000..bea451170 --- /dev/null +++ b/monkey/monkey_island/cc/services/telemetry/processing/scan.py @@ -0,0 +1,44 @@ +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 + + +def process_scan_telemetry(telemetry_json): + update_edges_and_nodes_based_on_scan_telemetry(telemetry_json) + test_open_data_endpoints(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): + edge = get_edge_by_scan_or_exploit_telemetry(telemetry_json) + data = copy.deepcopy(telemetry_json['data']['machine']) + ip_address = data.pop("ip_addr") + domain_name = data.pop("domain_name") + new_scan = \ + { + "timestamp": telemetry_json["timestamp"], + "data": data + } + mongo.db.edge.update( + {"_id": edge["_id"]}, + {"$push": {"scans": new_scan}, + "$set": {"ip_address": ip_address, 'domain_name': domain_name}} + ) + node = mongo.db.node.find_one({"_id": edge["to"]}) + if node is not None: + scan_os = new_scan["data"]["os"] + if "type" in scan_os: + mongo.db.node.update({"_id": node["_id"]}, + {"$set": {"os.type": scan_os["type"]}}, + upsert=False) + if "version" in scan_os: + mongo.db.node.update({"_id": node["_id"]}, + {"$set": {"os.version": scan_os["version"]}}, + upsert=False) diff --git a/monkey/monkey_island/cc/services/telemetry/processing/state.py b/monkey/monkey_island/cc/services/telemetry/processing/state.py new file mode 100644 index 000000000..4e164e900 --- /dev/null +++ b/monkey/monkey_island/cc/services/telemetry/processing/state.py @@ -0,0 +1,17 @@ +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 + + +def process_state_telemetry(telemetry_json): + monkey = NodeService.get_monkey_by_guid(telemetry_json['monkey_guid']) + NodeService.add_communication_info(monkey, telemetry_json['command_control_channel']) + if telemetry_json['data']['done']: + NodeService.set_monkey_dead(monkey, True) + else: + NodeService.set_monkey_dead(monkey, False) + + if telemetry_json['data']['done']: + current_monkey = Monkey.get_single_monkey_by_guid(telemetry_json['monkey_guid']) + test_passed_findings_for_unreached_segments(current_monkey) diff --git a/monkey/monkey_island/cc/services/telemetry/processing/system_info.py b/monkey/monkey_island/cc/services/telemetry/processing/system_info.py new file mode 100644 index 000000000..ebf11c219 --- /dev/null +++ b/monkey/monkey_island/cc/services/telemetry/processing/system_info.py @@ -0,0 +1,99 @@ +from monkey_island.cc.database import mongo +from monkey_island.cc.services import mimikatz_utils +from monkey_island.cc.services.node import NodeService +from monkey_island.cc.services.config import ConfigService +from monkey_island.cc.services.telemetry.zero_trust_tests.antivirus_existence import test_antivirus_existence +from monkey_island.cc.services.wmi_handler import WMIHandler +from monkey_island.cc.encryptor import encryptor + + +def process_system_info_telemetry(telemetry_json): + process_ssh_info(telemetry_json) + process_credential_info(telemetry_json) + process_mimikatz_and_wmi_info(telemetry_json) + process_aws_data(telemetry_json) + test_antivirus_existence(telemetry_json) + + +def process_ssh_info(telemetry_json): + if 'ssh_info' in telemetry_json['data']: + ssh_info = telemetry_json['data']['ssh_info'] + encrypt_system_info_ssh_keys(ssh_info) + if telemetry_json['data']['network_info']['networks']: + # We use user_name@machine_ip as the name of the ssh key stolen, thats why we need ip from telemetry + add_ip_to_ssh_keys(telemetry_json['data']['network_info']['networks'][0], ssh_info) + add_system_info_ssh_keys_to_config(ssh_info) + + +def add_system_info_ssh_keys_to_config(ssh_info): + for user in ssh_info: + ConfigService.creds_add_username(user['name']) + # Public key is useless without private key + if user['public_key'] and user['private_key']: + ConfigService.ssh_add_keys(user['public_key'], user['private_key'], + user['name'], user['ip']) + + +def add_ip_to_ssh_keys(ip, ssh_info): + for key in ssh_info: + key['ip'] = ip['addr'] + + +def encrypt_system_info_ssh_keys(ssh_info): + for idx, user in enumerate(ssh_info): + for field in ['public_key', 'private_key', 'known_hosts']: + if ssh_info[idx][field]: + ssh_info[idx][field] = encryptor.enc(ssh_info[idx][field].encode('utf-8')) + + +def process_credential_info(telemetry_json): + if 'credentials' in telemetry_json['data']: + creds = telemetry_json['data']['credentials'] + encrypt_system_info_creds(creds) + add_system_info_creds_to_config(creds) + replace_user_dot_with_comma(creds) + + +def replace_user_dot_with_comma(creds): + for user in creds: + if -1 != user.find('.'): + new_user = user.replace('.', ',') + creds[new_user] = creds.pop(user) + + +def add_system_info_creds_to_config(creds): + for user in creds: + ConfigService.creds_add_username(user) + if 'password' in creds[user]: + ConfigService.creds_add_password(creds[user]['password']) + if 'lm_hash' in creds[user]: + ConfigService.creds_add_lm_hash(creds[user]['lm_hash']) + if 'ntlm_hash' in creds[user]: + ConfigService.creds_add_ntlm_hash(creds[user]['ntlm_hash']) + + +def encrypt_system_info_creds(creds): + for user in creds: + for field in ['password', 'lm_hash', 'ntlm_hash']: + if field in creds[user]: + # this encoding is because we might run into passwords which are not pure ASCII + creds[user][field] = encryptor.enc(creds[user][field].encode('utf-8')) + + +def process_mimikatz_and_wmi_info(telemetry_json): + users_secrets = {} + if 'mimikatz' in telemetry_json['data']: + users_secrets = mimikatz_utils.MimikatzSecrets. \ + extract_secrets_from_mimikatz(telemetry_json['data'].get('mimikatz', '')) + if 'wmi' in telemetry_json['data']: + monkey_id = NodeService.get_monkey_by_guid(telemetry_json['monkey_guid']).get('_id') + wmi_handler = WMIHandler(monkey_id, telemetry_json['data']['wmi'], users_secrets) + wmi_handler.process_and_handle_wmi_info() + + +def process_aws_data(telemetry_json): + if 'aws' in telemetry_json['data']: + if 'instance_id' in telemetry_json['data']['aws']: + monkey_id = NodeService.get_monkey_by_guid(telemetry_json['monkey_guid']).get('_id') + mongo.db.monkey.update_one({'_id': monkey_id}, + {'$set': {'aws_instance_id': telemetry_json['data']['aws']['instance_id']}}) diff --git a/monkey/monkey_island/cc/services/telemetry/processing/tunnel.py b/monkey/monkey_island/cc/services/telemetry/processing/tunnel.py new file mode 100644 index 000000000..1598b144a --- /dev/null +++ b/monkey/monkey_island/cc/services/telemetry/processing/tunnel.py @@ -0,0 +1,13 @@ +from monkey_island.cc.services.node import NodeService +from monkey_island.cc.services.telemetry.processing.utils import get_tunnel_host_ip_from_proxy_field +from monkey_island.cc.services.telemetry.zero_trust_tests.tunneling import test_tunneling_violation + + +def process_tunnel_telemetry(telemetry_json): + test_tunneling_violation(telemetry_json) + monkey_id = NodeService.get_monkey_by_guid(telemetry_json['monkey_guid'])["_id"] + if telemetry_json['data']['proxy'] is not None: + tunnel_host_ip = get_tunnel_host_ip_from_proxy_field(telemetry_json) + NodeService.set_monkey_tunnel(monkey_id, tunnel_host_ip) + else: + NodeService.unset_all_monkey_tunnels(monkey_id) diff --git a/monkey/monkey_island/cc/services/telemetry/processing/utils.py b/monkey/monkey_island/cc/services/telemetry/processing/utils.py new file mode 100644 index 000000000..466b81bf1 --- /dev/null +++ b/monkey/monkey_island/cc/services/telemetry/processing/utils.py @@ -0,0 +1,18 @@ +from monkey_island.cc.services.edge import EdgeService +from monkey_island.cc.services.node import NodeService + + +def get_edge_by_scan_or_exploit_telemetry(telemetry_json): + dst_ip = telemetry_json['data']['machine']['ip_addr'] + dst_domain_name = telemetry_json['data']['machine']['domain_name'] + src_monkey = NodeService.get_monkey_by_guid(telemetry_json['monkey_guid']) + dst_node = NodeService.get_monkey_by_ip(dst_ip) + if dst_node is None: + dst_node = NodeService.get_or_create_node(dst_ip, dst_domain_name) + + return EdgeService.get_or_create_edge(src_monkey["_id"], dst_node["_id"]) + + +def get_tunnel_host_ip_from_proxy_field(telemetry_json): + tunnel_host_ip = telemetry_json['data']['proxy'].split(":")[-2].replace("//", "") + return tunnel_host_ip diff --git a/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/__init__.py b/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/antivirus_existence.py b/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/antivirus_existence.py new file mode 100644 index 000000000..b8b8c559b --- /dev/null +++ b/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/antivirus_existence.py @@ -0,0 +1,47 @@ +import json + +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 +from monkey_island.cc.models.zero_trust.event import Event +from monkey_island.cc.services.telemetry.zero_trust_tests.known_anti_viruses import ANTI_VIRUS_KNOWN_PROCESS_NAMES + + +def test_antivirus_existence(telemetry_json): + current_monkey = Monkey.get_single_monkey_by_guid(telemetry_json['monkey_guid']) + if 'process_list' in telemetry_json['data']: + process_list_event = Event.create_event( + title="Process list", + message="Monkey on {} scanned the process list".format(current_monkey.hostname), + event_type=EVENT_TYPE_MONKEY_LOCAL) + events = [process_list_event] + + av_processes = filter_av_processes(telemetry_json) + + for process in av_processes: + events.append(Event.create_event( + 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_MONKEY_LOCAL + )) + + if len(av_processes) > 0: + test_status = STATUS_PASSED + else: + test_status = STATUS_FAILED + AggregateFinding.create_or_add_to_existing( + test=TEST_ENDPOINT_SECURITY_EXISTS, status=test_status, events=events + ) + + +def filter_av_processes(telemetry_json): + all_processes = telemetry_json['data']['process_list'].items() + av_processes = [] + for process in all_processes: + process_name = process[1]['name'] + # This is for case-insensitive `in`. Generator expression is to save memory. + if process_name.upper() in (known_av_name.upper() for known_av_name in ANTI_VIRUS_KNOWN_PROCESS_NAMES): + av_processes.append(process) + return av_processes diff --git a/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/communicate_as_new_user.py b/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/communicate_as_new_user.py new file mode 100644 index 000000000..6c5b1154b --- /dev/null +++ b/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/communicate_as_new_user.py @@ -0,0 +1,37 @@ +from common.data.zero_trust_consts import EVENT_TYPE_MONKEY_NETWORK, STATUS_FAILED, TEST_COMMUNICATE_AS_NEW_USER, \ + STATUS_PASSED +from monkey_island.cc.models.zero_trust.aggregate_finding import AggregateFinding +from monkey_island.cc.models.zero_trust.event import Event + +COMM_AS_NEW_USER_FAILED_FORMAT = "Monkey on {} couldn't communicate as new user. Details: {}" +COMM_AS_NEW_USER_SUCCEEDED_FORMAT = \ + "New user created by Monkey on {} successfully tried to communicate with the internet. Details: {}" + + +def test_new_user_communication(current_monkey, success, message): + AggregateFinding.create_or_add_to_existing( + test=TEST_COMMUNICATE_AS_NEW_USER, + # If the monkey succeeded to create a user, then the test failed. + status=STATUS_FAILED if success else STATUS_PASSED, + events=[ + get_attempt_event(current_monkey), + get_result_event(current_monkey, message, success) + ] + ) + + +def get_attempt_event(current_monkey): + tried_to_communicate_event = Event.create_event( + title="Communicate as new user", + message="Monkey on {} tried to create a new user and communicate from it.".format(current_monkey.hostname), + event_type=EVENT_TYPE_MONKEY_NETWORK) + return tried_to_communicate_event + + +def get_result_event(current_monkey, message, success): + message_format = COMM_AS_NEW_USER_SUCCEEDED_FORMAT if success else COMM_AS_NEW_USER_FAILED_FORMAT + + return Event.create_event( + title="Communicate as new user", + message=message_format.format(current_monkey.hostname, message), + event_type=EVENT_TYPE_MONKEY_NETWORK) diff --git a/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/data_endpoints.py b/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/data_endpoints.py new file mode 100644 index 000000000..68a7f713d --- /dev/null +++ b/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/data_endpoints.py @@ -0,0 +1,70 @@ +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, add_malicious_activity_to_timeline +from monkey_island.cc.models.zero_trust.event import Event + +HTTP_SERVERS_SERVICES_NAMES = ['tcp-80'] + + +def test_open_data_endpoints(telemetry_json): + services = telemetry_json["data"]["machine"]["services"] + current_monkey = Monkey.get_single_monkey_by_guid(telemetry_json['monkey_guid']) + found_http_server_status = STATUS_PASSED + found_elastic_search_server = STATUS_PASSED + + events = [ + Event.create_event( + title="Scan Telemetry", + message="Monkey on {} tried to perform a network scan, the target was {}.".format( + current_monkey.hostname, + telemetry_json["data"]["machine"]["ip_addr"]), + event_type=EVENT_TYPE_MONKEY_NETWORK, + timestamp=telemetry_json["timestamp"] + ) + ] + + for service_name, service_data in services.items(): + events.append(Event.create_event( + title="Scan telemetry analysis", + message="Scanned service: {}.".format(service_name), + event_type=EVENT_TYPE_MONKEY_NETWORK + )) + if service_name in HTTP_SERVERS_SERVICES_NAMES: + found_http_server_status = STATUS_FAILED + events.append(Event.create_event( + title="Scan telemetry analysis", + message="Service {} on {} recognized as an open data endpoint! Service details: {}".format( + service_data["display_name"], + telemetry_json["data"]["machine"]["ip_addr"], + json.dumps(service_data) + ), + event_type=EVENT_TYPE_MONKEY_NETWORK + )) + if service_name == ES_SERVICE: + found_elastic_search_server = STATUS_FAILED + events.append(Event.create_event( + title="Scan telemetry analysis", + message="Service {} on {} recognized as an open data endpoint! Service details: {}".format( + service_data["display_name"], + telemetry_json["data"]["machine"]["ip_addr"], + json.dumps(service_data) + ), + event_type=EVENT_TYPE_MONKEY_NETWORK + )) + + AggregateFinding.create_or_add_to_existing( + test=TEST_DATA_ENDPOINT_HTTP, + status=found_http_server_status, + events=events + ) + + AggregateFinding.create_or_add_to_existing( + test=TEST_DATA_ENDPOINT_ELASTIC, + status=found_elastic_search_server, + events=events + ) + + add_malicious_activity_to_timeline(events) diff --git a/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/known_anti_viruses.py b/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/known_anti_viruses.py new file mode 100644 index 000000000..e5d7c2355 --- /dev/null +++ b/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/known_anti_viruses.py @@ -0,0 +1,87 @@ +ANTI_VIRUS_KNOWN_PROCESS_NAMES = [ + u"AvastSvc.exe", + u"AvastUI.exe", + u"avcenter.exe", + u"avconfig.exe", + u"avgcsrvx.exe", + u"avgidsagent.exe", + u"avgnt.exe", + u"avgrsx.exe", + u"avguard.exe", + u"avgui.exe", + u"avgwdsvc.exe", + u"avp.exe", + u"avscan.exe", + u"bdagent.exe", + u"ccuac.exe", + u"egui.exe", + u"hijackthis.exe", + u"instup.exe", + u"keyscrambler.exe", + u"mbam.exe", + u"mbamgui.exe", + u"mbampt.exe", + u"mbamscheduler.exe", + u"mbamservice.exe", + u"MpCmdRun.exe", + u"MSASCui.exe", + u"MsMpEng.exe", + u"rstrui.exe", + u"spybotsd.exe", + u"zlclient.exe", + u"SymCorpUI.exe", + u"ccSvcHst.exe", + u"ccApp.exe", + u"LUALL.exe", + u"SMC.exe", + u"SMCgui.exe", + u"Rtvscan.exe", + u"LuComServer.exe", + u"ProtectionUtilSurrogate.exe", + u"ClientRemote.exe", + u"SemSvc.exe", + u"SemLaunchSvc.exe", + u"sesmcontinst.exe", + u"LuCatalog.exe", + u"LUALL.exe", + u"LuCallbackProxy.exe", + u"LuComServer_3_3.exe", + u"httpd.exe", + u"dbisqlc.exe", + u"dbsrv16.exe", + u"semapisrv.exe", + u"snac64.exe", + u"AutoExcl.exe", + u"DoScan.exe", + u"nlnhook.exe", + u"SavUI.exe", + u"SepLiveUpdate.exe", + u"Smc.exe", + u"SmcGui.exe", + u"SymCorpUI.exe", + u"symerr.exe", + u"ccSvcHst.exe", + u"DevViewer.exe", + u"DWHWizrd.exe", + u"RtvStart.exe", + u"roru.exe", + u"WSCSAvNotifier", + # Guardicore Centra + # Linux + u"gc-agents-service", + u"gc-guest-agent", + u"gc-guardig", + u"gc-digger", + u"gc-fastpath", + u"gc-enforcement-agent", + u"gc-enforcement-channel", + u"gc-detection-agent", + # Windows + u"gc-guest-agent.exe", + u"gc-windig.exe", + u"gc-digger.exe", + u"gc-fastpath.exe", + u"gc-enforcement-channel.exe", + u"gc-enforcement-agent.exe", + u"gc-agent-ui.exe" +] diff --git a/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/machine_exploited.py b/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/machine_exploited.py new file mode 100644 index 000000000..454f3a7fe --- /dev/null +++ b/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/machine_exploited.py @@ -0,0 +1,39 @@ +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, add_malicious_activity_to_timeline +from monkey_island.cc.models.zero_trust.event import Event + + +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, + target_ip, + exploiter), + event_type=EVENT_TYPE_MONKEY_NETWORK, + timestamp=timestamp + ) + ] + status = STATUS_PASSED + if exploit_successful: + events.append( + Event.create_event( + title="Exploit success!", + message="Monkey on {} successfully exploited {} using {}.".format( + current_monkey.hostname, + target_ip, + exploiter), + event_type=EVENT_TYPE_MONKEY_NETWORK, + timestamp=timestamp) + ) + status = STATUS_FAILED + + AggregateFinding.create_or_add_to_existing( + test=TEST_MACHINE_EXPLOITED, + status=status, + events=events + ) + + add_malicious_activity_to_timeline(events) diff --git a/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/segmentation.py b/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/segmentation.py new file mode 100644 index 000000000..50e60e493 --- /dev/null +++ b/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/segmentation.py @@ -0,0 +1,110 @@ +import itertools +from six import text_type + +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 +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 test_segmentation_violation(current_monkey, target_ip): + # TODO - lower code duplication between this and report.py. + subnet_groups = get_config_network_segments_as_subnet_groups() + for subnet_group in subnet_groups: + subnet_pairs = itertools.product(subnet_group, subnet_group) + for subnet_pair in subnet_pairs: + source_subnet = subnet_pair[0] + target_subnet = subnet_pair[1] + if is_segmentation_violation(current_monkey, target_ip, source_subnet, target_subnet): + event = get_segmentation_violation_event(current_monkey, source_subnet, target_ip, target_subnet) + SegmentationFinding.create_or_add_to_existing_finding( + subnets=[source_subnet, target_subnet], + status=STATUS_FAILED, + segmentation_event=event + ) + + +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", + message=SEGMENTATION_VIOLATION_EVENT_TEXT.format( + hostname=current_monkey.hostname, + source_ip=get_ip_if_in_subnet(current_monkey.ip_addresses, NetworkRange.get_range_obj(source_subnet)), + source_seg=source_subnet, + target_ip=target_ip, + target_seg=target_subnet + ), + event_type=EVENT_TYPE_MONKEY_NETWORK + ) + + +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] + create_or_add_findings_for_all_pairs(flat_all_subnets, current_monkey) + + +def create_or_add_findings_for_all_pairs(all_subnets, current_monkey): + # Filter the subnets that this monkey is part of. + this_monkey_subnets = [] + for subnet in all_subnets: + if get_ip_if_in_subnet(current_monkey.ip_addresses, NetworkRange.get_range_obj(subnet)) is not None: + this_monkey_subnets.append(subnet) + + # Get all the other subnets. + other_subnets = list(set(all_subnets) - set(this_monkey_subnets)) + + # Calculate the cartesian product - (this monkey subnets X other subnets). These pairs are the pairs that the monkey + # should have tested. + all_subnets_pairs_for_this_monkey = itertools.product(this_monkey_subnets, other_subnets) + + for subnet_pair in all_subnets_pairs_for_this_monkey: + SegmentationFinding.create_or_add_to_existing_finding( + subnets=list(subnet_pair), + status=STATUS_PASSED, + 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 + ) diff --git a/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/test_segmentation_zt_tests.py b/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/test_segmentation_zt_tests.py new file mode 100644 index 000000000..5f986e3b5 --- /dev/null +++ b/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/test_segmentation_zt_tests.py @@ -0,0 +1,46 @@ +import uuid + +from common.data.zero_trust_consts import TEST_SEGMENTATION, STATUS_PASSED, STATUS_FAILED, \ + EVENT_TYPE_MONKEY_NETWORK +from monkey_island.cc.models import Monkey +from monkey_island.cc.models.zero_trust.event import Event +from monkey_island.cc.models.zero_trust.finding import Finding +from monkey_island.cc.models.zero_trust.segmentation_finding import SegmentationFinding +from monkey_island.cc.services.telemetry.zero_trust_tests.segmentation import create_or_add_findings_for_all_pairs +from monkey_island.cc.testing.IslandTestCase import IslandTestCase + +FIRST_SUBNET = "1.1.1.1" +SECOND_SUBNET = "2.2.2.0/24" +THIRD_SUBNET = "3.3.3.3-3.3.3.200" + + +class TestSegmentationTests(IslandTestCase): + def test_create_findings_for_all_done_pairs(self): + self.fail_if_not_testing_env() + self.clean_finding_db() + + all_subnets = [FIRST_SUBNET, SECOND_SUBNET, THIRD_SUBNET] + + monkey = Monkey( + guid=str(uuid.uuid4()), + ip_addresses=[FIRST_SUBNET]) + + # no findings + self.assertEquals(len(Finding.objects(test=TEST_SEGMENTATION)), 0) + + # This is like the monkey is done and sent done telem + create_or_add_findings_for_all_pairs(all_subnets, monkey) + + # There are 2 subnets in which the monkey is NOT + self.assertEquals(len(Finding.objects(test=TEST_SEGMENTATION, status=STATUS_PASSED)), 2) + + # This is a monkey from 2nd subnet communicated with 1st subnet. + SegmentationFinding.create_or_add_to_existing_finding( + [FIRST_SUBNET, SECOND_SUBNET], + STATUS_FAILED, + Event.create_event(title="sdf", message="asd", event_type=EVENT_TYPE_MONKEY_NETWORK) + ) + + self.assertEquals(len(Finding.objects(test=TEST_SEGMENTATION, status=STATUS_PASSED)), 1) + self.assertEquals(len(Finding.objects(test=TEST_SEGMENTATION, status=STATUS_FAILED)), 1) + self.assertEquals(len(Finding.objects(test=TEST_SEGMENTATION)), 2) diff --git a/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/tunneling.py b/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/tunneling.py new file mode 100644 index 000000000..ce34c2bb4 --- /dev/null +++ b/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/tunneling.py @@ -0,0 +1,27 @@ +from common.data.zero_trust_consts import TEST_TUNNELING, STATUS_FAILED, EVENT_TYPE_MONKEY_NETWORK +from monkey_island.cc.models import Monkey +from monkey_island.cc.models.zero_trust.aggregate_finding import AggregateFinding, add_malicious_activity_to_timeline +from monkey_island.cc.models.zero_trust.event import Event +from monkey_island.cc.services.telemetry.processing.utils import get_tunnel_host_ip_from_proxy_field + + +def test_tunneling_violation(tunnel_telemetry_json): + if tunnel_telemetry_json['data']['proxy'] is not None: + # Monkey is tunneling, create findings + tunnel_host_ip = get_tunnel_host_ip_from_proxy_field(tunnel_telemetry_json) + current_monkey = Monkey.get_single_monkey_by_guid(tunnel_telemetry_json['monkey_guid']) + tunneling_events = [Event.create_event( + title="Tunneling event", + message="Monkey on {hostname} tunneled traffic through {proxy}.".format( + hostname=current_monkey.hostname, proxy=tunnel_host_ip), + event_type=EVENT_TYPE_MONKEY_NETWORK, + timestamp=tunnel_telemetry_json['timestamp'] + )] + + AggregateFinding.create_or_add_to_existing( + test=TEST_TUNNELING, + status=STATUS_FAILED, + events=tunneling_events + ) + + add_malicious_activity_to_timeline(tunneling_events) diff --git a/monkey/monkey_island/cc/testing/IslandTestCase.py b/monkey/monkey_island/cc/testing/IslandTestCase.py index e894f13df..6bca20f4a 100644 --- a/monkey/monkey_island/cc/testing/IslandTestCase.py +++ b/monkey/monkey_island/cc/testing/IslandTestCase.py @@ -1,6 +1,7 @@ import unittest from monkey_island.cc.environment.environment import env from monkey_island.cc.models import Monkey +from monkey_island.cc.models.zero_trust.finding import Finding class IslandTestCase(unittest.TestCase): @@ -10,3 +11,7 @@ class IslandTestCase(unittest.TestCase): @staticmethod def clean_monkey_db(): Monkey.objects().delete() + + @staticmethod + def clean_finding_db(): + Finding.objects().delete() diff --git a/monkey/monkey_island/cc/ui/package-lock.json b/monkey/monkey_island/cc/ui/package-lock.json index 934b567e7..f366d73bd 100644 --- a/monkey/monkey_island/cc/ui/package-lock.json +++ b/monkey/monkey_island/cc/ui/package-lock.json @@ -56,7 +56,7 @@ "@babel/helper-module-imports": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.0.0.tgz", - "integrity": "sha1-lggbcRHkhtpNLNlxrRpP4hbMLj0=", + "integrity": "sha512-aP/hlLq01DWNEiDg4Jn23i+CXxW/owM4WpDLFUbpjxe4NS3BhLVZQ5i7E0ZrxuQ/vwekIeciyamgB1UIYxxM6A==", "requires": { "@babel/types": "^7.0.0" }, @@ -74,7 +74,7 @@ "lodash": { "version": "4.17.11", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", - "integrity": "sha1-s56mIp72B+zYniyN8SU2iRysm40=" + "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==" }, "to-fast-properties": { "version": "2.0.0", @@ -259,7 +259,7 @@ "@emotion/cache": { "version": "10.0.9", "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-10.0.9.tgz", - "integrity": "sha1-4Me3oon3Uw7c+tTc84WL0uVwCm8=", + "integrity": "sha512-f7MblpE2xoimC4fCMZ9pivmsIn7hyWRIvY75owMDi8pdOSeh+w5tH3r4hBJv/LLrwiMM7cTQURqTPcYoL5pWnw==", "requires": { "@emotion/sheet": "0.9.2", "@emotion/stylis": "0.8.3", @@ -270,7 +270,7 @@ "@emotion/core": { "version": "10.0.10", "resolved": "https://registry.npmjs.org/@emotion/core/-/core-10.0.10.tgz", - "integrity": "sha1-jTEU5aL4sXinBnxgOik3UW8YCwg=", + "integrity": "sha512-U1aE2cOWUscjc8ZJ3Cx32udOzLeRoJwGxBH93xQD850oQFpwPKZARzdUtdc9SByUOwzSFYxhDhrpXnV34FJmWg==", "requires": { "@emotion/cache": "^10.0.9", "@emotion/css": "^10.0.9", @@ -292,12 +292,12 @@ "@emotion/hash": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.7.1.tgz", - "integrity": "sha1-mDNyI0E3n7fWfwaksAqzw3kT2lM=" + "integrity": "sha512-OYpa/Sg+2GDX+jibUfpZVn1YqSVRpYmTLF2eyAfrFTIJSbwyIrc+YscayoykvaOME/wV4BV0Sa0yqdMrgse6mA==" }, "@emotion/memoize": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.1.tgz", - "integrity": "sha1-6TwTlCWSz17wGqgpdETcGSvu5S8=" + "integrity": "sha512-Qv4LTqO11jepd5Qmlp3M1YEjBumoTHcHFdgPTQ+sFlIL5myi/7xu/POwP7IRu6odBdmLXdtIs1D6TuW6kbwbbg==" }, "@emotion/serialize": { "version": "0.11.6", @@ -314,27 +314,27 @@ "@emotion/sheet": { "version": "0.9.2", "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-0.9.2.tgz", - "integrity": "sha1-dOXGteSJobowqyRqte7dlpFkh8Q=" + "integrity": "sha512-pVBLzIbC/QCHDKJF2E82V2H/W/B004mDFQZiyo/MSR+VC4pV5JLG0TF/zgQDFvP3fZL/5RTPGEmXlYJBMUuJ+A==" }, "@emotion/stylis": { "version": "0.8.3", "resolved": "https://registry.npmjs.org/@emotion/stylis/-/stylis-0.8.3.tgz", - "integrity": "sha1-PKfpvLMbPLSvuutmFW2G7oXiMkY=" + "integrity": "sha512-M3nMfJ6ndJMYloSIbYEBq6G3eqoYD41BpDOxreE8j0cb4fzz/5qvmqU9Mb2hzsXcCnIlGlWhS03PCzVGvTAe0Q==" }, "@emotion/unitless": { "version": "0.7.3", "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.3.tgz", - "integrity": "sha1-YxCgR/EtIaEDb7AxMXIZiSRAQW8=" + "integrity": "sha512-4zAPlpDEh2VwXswwr/t8xGNDGg8RQiPxtxZ3qQEXyQsBV39ptTdESCjuBvGze1nLMVrxmTIKmnO/nAV8Tqjjzg==" }, "@emotion/utils": { "version": "0.11.1", "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-0.11.1.tgz", - "integrity": "sha1-hSm3QSputLSL325yDMG45uHhdig=" + "integrity": "sha512-8M3VN0hetwhsJ8dH8VkVy7xo5/1VoBsDOk/T4SJOeXwTO1c4uIqVNx2qyecLFnnUWD5vvUqHQ1gASSeUN6zcTg==" }, "@emotion/weak-memoize": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.2.2.tgz", - "integrity": "sha1-Y5hdPYsCUw4IaZYvTaCRQu6OIA4=" + "integrity": "sha512-n/VQ4mbfr81aqkx/XmVicOLjviMuy02eenSdJY33SVA7S2J42EU0P1H0mOogfYedb3wXA0d/LVtBrgTSm04WEA==" }, "@kunukn/react-collapse": { "version": "1.0.5", @@ -630,7 +630,6 @@ "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=", "dev": true, - "optional": true, "requires": { "kind-of": "^3.0.2", "longest": "^1.0.1", @@ -689,7 +688,7 @@ "are-we-there-yet": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", - "integrity": "sha1-SzXClE8GKov82mZBB2A1D+nd/CE=", + "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", "requires": { "delegates": "^1.0.0", "readable-stream": "^2.0.6" @@ -703,12 +702,12 @@ "process-nextick-args": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", - "integrity": "sha1-o31zL0JxtKsa0HDTVQjoKQeI/6o=" + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" }, "readable-stream": { "version": "2.3.6", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha1-sRwn2IuP8fvgcGQ8+UsMea4bCq8=", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -722,7 +721,7 @@ "string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha1-nPFhG6YmhdcDCunkujQUnDrwP8g=", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "requires": { "safe-buffer": "~5.1.0" } @@ -3004,7 +3003,7 @@ "clone-deep": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-2.0.2.tgz", - "integrity": "sha1-ANs6Hhc2VnMNEYjD1qztbX6pdxM=", + "integrity": "sha512-SZegPTKjCgpQH63E+eN6mVEEPdQBOUzjyJm5Pora4lrwWRFS8I0QAxV/KD6vV/i0WuijHZWQC1fMsPEdxfdVCQ==", "requires": { "for-own": "^1.0.0", "is-plain-object": "^2.0.4", @@ -3023,7 +3022,7 @@ "kind-of": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha1-ARRrNqYhjmTljzqNZt5df8b20FE=" + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" } } }, @@ -3088,8 +3087,7 @@ "commander": { "version": "2.15.1", "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", - "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", - "dev": true + "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==" }, "commondir": { "version": "1.0.1", @@ -3313,7 +3311,7 @@ "copy-to-clipboard": { "version": "3.0.8", "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.0.8.tgz", - "integrity": "sha1-9OgvSogw3ORma3643tDJvMMTq6k=", + "integrity": "sha512-c3GdeY8qxCHGezVb1EFQfHYK/8NZRemgcTIzPq7PuxjHAf/raKibn2QdhHPb/y6q74PMgH6yizaDZlRmw6QyKw==", "requires": { "toggle-selection": "^1.0.3" } @@ -3369,7 +3367,7 @@ }, "yargs": { "version": "11.1.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-11.1.0.tgz", + "resolved": "http://registry.npmjs.org/yargs/-/yargs-11.1.0.tgz", "integrity": "sha512-NwW69J42EsCSanF8kyn5upxvjp5ds+t3+udGBeTbFnERA+lF541DDpMawzo4z6W/QrzNM18D+BPMiOBibnFV5A==", "dev": true, "requires": { @@ -3420,8 +3418,8 @@ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", "requires": { - "argparse": "1.0.9", - "esprima": "4.0.1" + "argparse": "^1.0.7", + "esprima": "^4.0.0" } }, "parse-json": { @@ -3429,8 +3427,8 @@ "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", "requires": { - "error-ex": "1.3.2", - "json-parse-better-errors": "1.0.2" + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" } } } @@ -3658,6 +3656,270 @@ "es5-ext": "^0.10.9" } }, + "d3": { + "version": "5.11.0", + "resolved": "https://registry.npmjs.org/d3/-/d3-5.11.0.tgz", + "integrity": "sha512-LXgMVUAEAzQh6WfEEOa8tJX4RA64ZJ6twC3CJ+Xzid+fXWLTZkkglagXav/eOoQgzQi5rzV0xC4Sfspd6hFDHA==", + "requires": { + "d3-array": "1", + "d3-axis": "1", + "d3-brush": "1", + "d3-chord": "1", + "d3-collection": "1", + "d3-color": "1", + "d3-contour": "1", + "d3-dispatch": "1", + "d3-drag": "1", + "d3-dsv": "1", + "d3-ease": "1", + "d3-fetch": "1", + "d3-force": "1", + "d3-format": "1", + "d3-geo": "1", + "d3-hierarchy": "1", + "d3-interpolate": "1", + "d3-path": "1", + "d3-polygon": "1", + "d3-quadtree": "1", + "d3-random": "1", + "d3-scale": "2", + "d3-scale-chromatic": "1", + "d3-selection": "1", + "d3-shape": "1", + "d3-time": "1", + "d3-time-format": "2", + "d3-timer": "1", + "d3-transition": "1", + "d3-voronoi": "1", + "d3-zoom": "1" + } + }, + "d3-array": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-1.2.4.tgz", + "integrity": "sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw==" + }, + "d3-axis": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-1.0.12.tgz", + "integrity": "sha512-ejINPfPSNdGFKEOAtnBtdkpr24c4d4jsei6Lg98mxf424ivoDP2956/5HDpIAtmHo85lqT4pruy+zEgvRUBqaQ==" + }, + "d3-brush": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-1.1.3.tgz", + "integrity": "sha512-v8bbYyCFKjyCzFk/tdWqXwDykY8YWqhXYjcYxfILIit085VZOpj4XJKOMccTsvWxgzSLMJQg5SiqHjslsipEDg==", + "requires": { + "d3-dispatch": "1", + "d3-drag": "1", + "d3-interpolate": "1", + "d3-selection": "1", + "d3-transition": "1" + } + }, + "d3-chord": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-1.0.6.tgz", + "integrity": "sha512-JXA2Dro1Fxw9rJe33Uv+Ckr5IrAa74TlfDEhE/jfLOaXegMQFQTAgAw9WnZL8+HxVBRXaRGCkrNU7pJeylRIuA==", + "requires": { + "d3-array": "1", + "d3-path": "1" + } + }, + "d3-collection": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/d3-collection/-/d3-collection-1.0.7.tgz", + "integrity": "sha512-ii0/r5f4sjKNTfh84Di+DpztYwqKhEyUlKoPrzUFfeSkWxjW49xU2QzO9qrPrNkpdI0XJkfzvmTu8V2Zylln6A==" + }, + "d3-color": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-1.3.0.tgz", + "integrity": "sha512-NHODMBlj59xPAwl2BDiO2Mog6V+PrGRtBfWKqKRrs9MCqlSkIEb0Z/SfY7jW29ReHTDC/j+vwXhnZcXI3+3fbg==" + }, + "d3-contour": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-1.3.2.tgz", + "integrity": "sha512-hoPp4K/rJCu0ladiH6zmJUEz6+u3lgR+GSm/QdM2BBvDraU39Vr7YdDCicJcxP1z8i9B/2dJLgDC1NcvlF8WCg==", + "requires": { + "d3-array": "^1.1.1" + } + }, + "d3-dispatch": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-1.0.5.tgz", + "integrity": "sha512-vwKx+lAqB1UuCeklr6Jh1bvC4SZgbSqbkGBLClItFBIYH4vqDJCA7qfoy14lXmJdnBOdxndAMxjCbImJYW7e6g==" + }, + "d3-drag": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-1.2.4.tgz", + "integrity": "sha512-ICPurDETFAelF1CTHdIyiUM4PsyZLaM+7oIBhmyP+cuVjze5vDZ8V//LdOFjg0jGnFIZD/Sfmk0r95PSiu78rw==", + "requires": { + "d3-dispatch": "1", + "d3-selection": "1" + } + }, + "d3-dsv": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-1.1.1.tgz", + "integrity": "sha512-1EH1oRGSkeDUlDRbhsFytAXU6cAmXFzc52YUe6MRlPClmWb85MP1J5x+YJRzya4ynZWnbELdSAvATFW/MbxaXw==", + "requires": { + "commander": "2", + "iconv-lite": "0.4", + "rw": "1" + } + }, + "d3-ease": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-1.0.5.tgz", + "integrity": "sha512-Ct1O//ly5y5lFM9YTdu+ygq7LleSgSE4oj7vUt9tPLHUi8VCV7QoizGpdWRWAwCO9LdYzIrQDg97+hGVdsSGPQ==" + }, + "d3-fetch": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-1.1.2.tgz", + "integrity": "sha512-S2loaQCV/ZeyTyIF2oP8D1K9Z4QizUzW7cWeAOAS4U88qOt3Ucf6GsmgthuYSdyB2HyEm4CeGvkQxWsmInsIVA==", + "requires": { + "d3-dsv": "1" + } + }, + "d3-force": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-1.2.1.tgz", + "integrity": "sha512-HHvehyaiUlVo5CxBJ0yF/xny4xoaxFxDnBXNvNcfW9adORGZfyNF1dj6DGLKyk4Yh3brP/1h3rnDzdIAwL08zg==", + "requires": { + "d3-collection": "1", + "d3-dispatch": "1", + "d3-quadtree": "1", + "d3-timer": "1" + } + }, + "d3-format": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-1.3.2.tgz", + "integrity": "sha512-Z18Dprj96ExragQ0DeGi+SYPQ7pPfRMtUXtsg/ChVIKNBCzjO8XYJvRTC1usblx52lqge56V5ect+frYTQc8WQ==" + }, + "d3-geo": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-1.11.6.tgz", + "integrity": "sha512-z0J8InXR9e9wcgNtmVnPTj0TU8nhYT6lD/ak9may2PdKqXIeHUr8UbFLoCtrPYNsjv6YaLvSDQVl578k6nm7GA==", + "requires": { + "d3-array": "1" + } + }, + "d3-hierarchy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-1.1.8.tgz", + "integrity": "sha512-L+GHMSZNwTpiq4rt9GEsNcpLa4M96lXMR8M/nMG9p5hBE0jy6C+3hWtyZMenPQdwla249iJy7Nx0uKt3n+u9+w==" + }, + "d3-interpolate": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-1.3.2.tgz", + "integrity": "sha512-NlNKGopqaz9qM1PXh9gBF1KSCVh+jSFErrSlD/4hybwoNX/gt1d8CDbDW+3i+5UOHhjC6s6nMvRxcuoMVNgL2w==", + "requires": { + "d3-color": "1" + } + }, + "d3-path": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.8.tgz", + "integrity": "sha512-J6EfUNwcMQ+aM5YPOB8ZbgAZu6wc82f/0WFxrxwV6Ll8wBwLaHLKCqQ5Imub02JriCVVdPjgI+6P3a4EWJCxAg==" + }, + "d3-polygon": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-1.0.5.tgz", + "integrity": "sha512-RHhh1ZUJZfhgoqzWWuRhzQJvO7LavchhitSTHGu9oj6uuLFzYZVeBzaWTQ2qSO6bz2w55RMoOCf0MsLCDB6e0w==" + }, + "d3-quadtree": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-1.0.6.tgz", + "integrity": "sha512-NUgeo9G+ENQCQ1LsRr2qJg3MQ4DJvxcDNCiohdJGHt5gRhBW6orIB5m5FJ9kK3HNL8g9F4ERVoBzcEwQBfXWVA==" + }, + "d3-random": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-1.1.2.tgz", + "integrity": "sha512-6AK5BNpIFqP+cx/sreKzNjWbwZQCSUatxq+pPRmFIQaWuoD+NrbVWw7YWpHiXpCQ/NanKdtGDuB+VQcZDaEmYQ==" + }, + "d3-scale": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-2.2.2.tgz", + "integrity": "sha512-LbeEvGgIb8UMcAa0EATLNX0lelKWGYDQiPdHj+gLblGVhGLyNbaCn3EvrJf0A3Y/uOOU5aD6MTh5ZFCdEwGiCw==", + "requires": { + "d3-array": "^1.2.0", + "d3-collection": "1", + "d3-format": "1", + "d3-interpolate": "1", + "d3-time": "1", + "d3-time-format": "2" + } + }, + "d3-scale-chromatic": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-1.5.0.tgz", + "integrity": "sha512-ACcL46DYImpRFMBcpk9HhtIyC7bTBR4fNOPxwVSl0LfulDAwyiHyPOTqcDG1+t5d4P9W7t/2NAuWu59aKko/cg==", + "requires": { + "d3-color": "1", + "d3-interpolate": "1" + } + }, + "d3-selection": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-1.4.0.tgz", + "integrity": "sha512-EYVwBxQGEjLCKF2pJ4+yrErskDnz5v403qvAid96cNdCMr8rmCYfY5RGzWz24mdIbxmDf6/4EAH+K9xperD5jg==" + }, + "d3-shape": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.5.tgz", + "integrity": "sha512-VKazVR3phgD+MUCldapHD7P9kcrvPcexeX/PkMJmkUov4JM8IxsSg1DvbYoYich9AtdTsa5nNk2++ImPiDiSxg==", + "requires": { + "d3-path": "1" + } + }, + "d3-time": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-1.0.11.tgz", + "integrity": "sha512-Z3wpvhPLW4vEScGeIMUckDW7+3hWKOQfAWg/U7PlWBnQmeKQ00gCUsTtWSYulrKNA7ta8hJ+xXc6MHrMuITwEw==" + }, + "d3-time-format": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-2.1.3.tgz", + "integrity": "sha512-6k0a2rZryzGm5Ihx+aFMuO1GgelgIz+7HhB4PH4OEndD5q2zGn1mDfRdNrulspOfR6JXkb2sThhDK41CSK85QA==", + "requires": { + "d3-time": "1" + } + }, + "d3-timer": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-1.0.9.tgz", + "integrity": "sha512-rT34J5HnQUHhcLvhSB9GjCkN0Ddd5Y8nCwDBG2u6wQEeYxT/Lf51fTFFkldeib/sE/J0clIe0pnCfs6g/lRbyg==" + }, + "d3-transition": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-1.2.0.tgz", + "integrity": "sha512-VJ7cmX/FPIPJYuaL2r1o1EMHLttvoIuZhhuAlRoOxDzogV8iQS6jYulDm3xEU3TqL80IZIhI551/ebmCMrkvhw==", + "requires": { + "d3-color": "1", + "d3-dispatch": "1", + "d3-ease": "1", + "d3-interpolate": "1", + "d3-selection": "^1.1.0", + "d3-timer": "1" + } + }, + "d3-voronoi": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/d3-voronoi/-/d3-voronoi-1.1.4.tgz", + "integrity": "sha512-dArJ32hchFsrQ8uMiTBLq256MpnZjeuBtdHpaDlYuQyjU0CVzCJl/BVW+SkszaAeH95D/8gxqAhgx0ouAWAfRg==" + }, + "d3-zoom": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-1.8.3.tgz", + "integrity": "sha512-VoLXTK4wvy1a0JpH2Il+F2CiOhVu7VRXWF5M/LroMIh3/zBAC3WAt7QoIvPibOavVo20hN6/37vwAsdBejLyKQ==", + "requires": { + "d3-dispatch": "1", + "d3-drag": "1", + "d3-interpolate": "1", + "d3-selection": "1", + "d3-transition": "1" + } + }, "dashdash": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", @@ -5199,6 +5461,11 @@ } } }, + "file-saver": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.2.tgz", + "integrity": "sha512-Wz3c3XQ5xroCxd1G8b7yL0Ehkf0TC9oYC6buPFkNnU9EnaPlifeAFCyCh+iewXTyFRcg0a6j3J7FmJsIhlhBdw==" + }, "filename-regex": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz", @@ -5271,7 +5538,7 @@ "find-root": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", - "integrity": "sha1-q8/Iunb3CMQql7PWhbfpRQv7nOQ=" + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==" }, "find-up": { "version": "1.1.2", @@ -5523,15 +5790,14 @@ "dev": true, "optional": true, "requires": { - "co": "4.6.0", - "json-stable-stringify": "1.0.1" + "co": "^4.6.0", + "json-stable-stringify": "^1.0.1" } }, "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "aproba": { "version": "1.1.1", @@ -5582,8 +5848,7 @@ "balanced-match": { "version": "0.4.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "bcrypt-pbkdf": { "version": "1.0.1", @@ -5598,7 +5863,6 @@ "version": "0.0.9", "bundled": true, "dev": true, - "optional": true, "requires": { "inherits": "~2.0.0" } @@ -5607,7 +5871,6 @@ "version": "2.10.1", "bundled": true, "dev": true, - "optional": true, "requires": { "hoek": "2.x.x" } @@ -5616,7 +5879,6 @@ "version": "1.1.7", "bundled": true, "dev": true, - "optional": true, "requires": { "balanced-match": "^0.4.1", "concat-map": "0.0.1" @@ -5625,8 +5887,7 @@ "buffer-shims": { "version": "1.0.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "caseless": { "version": "0.12.0", @@ -5643,14 +5904,12 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "combined-stream": { "version": "1.0.5", "bundled": true, "dev": true, - "optional": true, "requires": { "delayed-stream": "~1.0.0" } @@ -5658,20 +5917,17 @@ "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "core-util-is": { "version": "1.0.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "cryptiles": { "version": "2.0.5", @@ -5717,8 +5973,7 @@ "delayed-stream": { "version": "1.0.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "delegates": { "version": "1.0.0", @@ -5732,7 +5987,7 @@ "dev": true, "optional": true, "requires": { - "jsbn": "0.1.1" + "jsbn": "~0.1.0" } }, "extend": { @@ -5744,8 +5999,7 @@ "extsprintf": { "version": "1.0.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "forever-agent": { "version": "0.6.1", @@ -5767,14 +6021,12 @@ "fs.realpath": { "version": "1.0.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "fstream": { "version": "1.0.11", "bundled": true, "dev": true, - "optional": true, "requires": { "graceful-fs": "^4.1.2", "inherits": "~2.0.0", @@ -5830,7 +6082,6 @@ "version": "7.1.2", "bundled": true, "dev": true, - "optional": true, "requires": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -5843,8 +6094,7 @@ "graceful-fs": { "version": "4.1.11", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "har-schema": { "version": "1.0.5", @@ -5858,8 +6108,8 @@ "dev": true, "optional": true, "requires": { - "ajv": "4.11.8", - "har-schema": "1.0.5" + "ajv": "^4.9.1", + "har-schema": "^1.0.5" } }, "has-unicode": { @@ -5883,8 +6133,7 @@ "hoek": { "version": "2.16.3", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "http-signature": { "version": "1.1.1", @@ -5901,7 +6150,6 @@ "version": "1.0.6", "bundled": true, "dev": true, - "optional": true, "requires": { "once": "^1.3.0", "wrappy": "1" @@ -5910,8 +6158,7 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "ini": { "version": "1.3.4", @@ -5923,7 +6170,6 @@ "version": "1.0.0", "bundled": true, "dev": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -5937,8 +6183,7 @@ "isarray": { "version": "1.0.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "isstream": { "version": "0.1.2", @@ -5952,7 +6197,7 @@ "dev": true, "optional": true, "requires": { - "jsbn": "0.1.1" + "jsbn": "~0.1.0" } }, "jsbn": { @@ -5973,7 +6218,7 @@ "dev": true, "optional": true, "requires": { - "jsonify": "0.0.0" + "jsonify": "~0.0.0" } }, "json-stringify-safe": { @@ -6011,14 +6256,12 @@ "mime-db": { "version": "1.27.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "mime-types": { "version": "2.1.15", "bundled": true, "dev": true, - "optional": true, "requires": { "mime-db": "~1.27.0" } @@ -6027,7 +6270,6 @@ "version": "3.0.4", "bundled": true, "dev": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -6035,14 +6277,12 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "mkdirp": { "version": "0.5.1", "bundled": true, "dev": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -6095,8 +6335,7 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "oauth-sign": { "version": "0.8.2", @@ -6114,7 +6353,6 @@ "version": "1.4.0", "bundled": true, "dev": true, - "optional": true, "requires": { "wrappy": "1" } @@ -6144,8 +6382,7 @@ "path-is-absolute": { "version": "1.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "performance-now": { "version": "0.2.0", @@ -6156,8 +6393,7 @@ "process-nextick-args": { "version": "1.0.7", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "punycode": { "version": "1.4.1", @@ -6195,7 +6431,6 @@ "version": "2.2.9", "bundled": true, "dev": true, - "optional": true, "requires": { "buffer-shims": "~1.0.0", "core-util-is": "~1.0.0", @@ -6240,7 +6475,6 @@ "version": "2.6.1", "bundled": true, "dev": true, - "optional": true, "requires": { "glob": "^7.0.5" } @@ -6248,8 +6482,7 @@ "safe-buffer": { "version": "5.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "semver": { "version": "5.3.0", @@ -6307,7 +6540,6 @@ "version": "1.0.2", "bundled": true, "dev": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -6318,7 +6550,6 @@ "version": "1.0.1", "bundled": true, "dev": true, - "optional": true, "requires": { "safe-buffer": "^5.0.1" } @@ -6333,7 +6564,6 @@ "version": "3.0.1", "bundled": true, "dev": true, - "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -6348,7 +6578,6 @@ "version": "2.2.1", "bundled": true, "dev": true, - "optional": true, "requires": { "block-stream": "*", "fstream": "^1.0.2", @@ -6404,8 +6633,7 @@ "util-deprecate": { "version": "1.0.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "uuid": { "version": "3.0.1", @@ -6434,15 +6662,14 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true, - "optional": true + "dev": true } } }, "fstream": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", - "integrity": "sha1-Touo7i1Ivk99DeUFRVVI6uWTIEU=", + "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", "requires": { "graceful-fs": "^4.1.2", "inherits": "~2.0.0", @@ -6500,7 +6727,7 @@ "gaze": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/gaze/-/gaze-1.1.3.tgz", - "integrity": "sha1-xEFzPhO5J6yMD/C0w7Az8ogSkko=", + "integrity": "sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g==", "requires": { "globule": "^1.0.0" } @@ -6570,7 +6797,6 @@ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz", "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=", "dev": true, - "optional": true, "requires": { "is-glob": "^2.0.0" } @@ -6614,7 +6840,7 @@ "globule": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/globule/-/globule-1.2.1.tgz", - "integrity": "sha1-Xf+xsZHyLSB5epNptJ6rTpg5aW0=", + "integrity": "sha512-g7QtgWF4uYSL5/dn71WxubOrS7JVGCnFPEnoeChJmBnyR9Mw8nGoEwOgJL/RC2Te0WhbsEUCejfH8SZNJ+adYQ==", "requires": { "glob": "~7.1.1", "lodash": "~4.17.10", @@ -7820,8 +8046,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", - "dev": true, - "optional": true + "dev": true }, "is-finite": { "version": "1.0.2", @@ -7841,7 +8066,6 @@ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", "dev": true, - "optional": true, "requires": { "is-extglob": "^1.0.0" } @@ -7906,8 +8130,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-2.0.0.tgz", "integrity": "sha1-IHurkWOEmcB7Kt8kCkGochADRXU=", - "dev": true, - "optional": true + "dev": true }, "is-promise": { "version": "2.1.0", @@ -8070,7 +8293,7 @@ "js-base64": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.5.1.tgz", - "integrity": "sha1-Hvo57yxfeYC7F4St5KivLeMpESE=" + "integrity": "sha512-M7kLczedRMYX4L8Mdh4MzyAMM9O5osx+4FcOQuTvr3A9F2D9S5JXheN0ewNbrvK2UatkTRhL5ejGmGSjNMiZuw==" }, "js-file-download": { "version": "0.4.4", @@ -8459,8 +8682,7 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "aproba": { "version": "1.2.0", @@ -8481,14 +8703,12 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -8503,20 +8723,17 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "core-util-is": { "version": "1.0.2", @@ -8617,7 +8834,7 @@ "dev": true, "optional": true, "requires": { - "minimatch": "3.0.4" + "minimatch": "^3.0.4" } }, "inflight": { @@ -8633,8 +8850,7 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "ini": { "version": "1.3.5", @@ -8646,9 +8862,8 @@ "version": "1.0.0", "bundled": true, "dev": true, - "optional": true, "requires": { - "number-is-nan": "1.0.1" + "number-is-nan": "^1.0.0" } }, "isarray": { @@ -8661,7 +8876,6 @@ "version": "3.0.4", "bundled": true, "dev": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -8669,14 +8883,12 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "minipass": { "version": "2.2.4", "bundled": true, "dev": true, - "optional": true, "requires": { "safe-buffer": "^5.1.1", "yallist": "^3.0.0" @@ -8695,7 +8907,6 @@ "version": "0.5.1", "bundled": true, "dev": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -8757,8 +8968,8 @@ "dev": true, "optional": true, "requires": { - "ignore-walk": "3.0.1", - "npm-bundled": "1.0.3" + "ignore-walk": "^3.0.1", + "npm-bundled": "^1.0.1" } }, "npmlog": { @@ -8776,8 +8987,7 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "object-assign": { "version": "4.1.1", @@ -8789,7 +8999,6 @@ "version": "1.4.0", "bundled": true, "dev": true, - "optional": true, "requires": { "wrappy": "1" } @@ -8875,8 +9084,7 @@ "safe-buffer": { "version": "5.1.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "safer-buffer": { "version": "2.1.2", @@ -8912,7 +9120,6 @@ "version": "1.0.2", "bundled": true, "dev": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -8932,7 +9139,6 @@ "version": "3.0.1", "bundled": true, "dev": true, - "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -8976,14 +9182,12 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "yallist": { "version": "3.0.2", "bundled": true, - "dev": true, - "optional": true + "dev": true } } }, @@ -9554,8 +9758,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=", - "dev": true, - "optional": true + "dev": true }, "loose-envify": { "version": "1.3.1", @@ -10100,7 +10303,7 @@ "node-fetch": { "version": "1.7.3", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz", - "integrity": "sha1-mA9vcthSEaU0fGsrwYxbhMPrR+8=", + "integrity": "sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ==", "requires": { "encoding": "^0.1.11", "is-stream": "^1.0.1" @@ -10115,7 +10318,7 @@ "node-gyp": { "version": "3.8.0", "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-3.8.0.tgz", - "integrity": "sha1-VAMEJhwzDoDQ1e3OJTpoyzlkIYw=", + "integrity": "sha512-3g8lYefrRRzvGeSowdJKAKyks8oUpLEd/DyPV4eMhVlhJ0aNaZqIrNUIPuEWWTAoPqyFkfGrM67MC69baqn6vA==", "requires": { "fstream": "^1.0.0", "glob": "^7.0.3", @@ -10134,7 +10337,7 @@ "ajv": { "version": "6.10.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.0.tgz", - "integrity": "sha1-kNDVRDnaWHzX6EO/twRfUL0ivfE=", + "integrity": "sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg==", "requires": { "fast-deep-equal": "^2.0.1", "fast-json-stable-stringify": "^2.0.0", @@ -10145,12 +10348,12 @@ "aws4": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", - "integrity": "sha1-8OAD2cqef1nHpQiUXXsu+aBKVC8=" + "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==" }, "extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha1-+LETa0Bx+9jrFAr/hYsQGewpFfo=" + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" }, "fast-deep-equal": { "version": "2.0.1", @@ -10160,7 +10363,7 @@ "har-validator": { "version": "5.1.3", "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", - "integrity": "sha1-HvievT5JllV2de7ZiTEQ3DUPoIA=", + "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", "requires": { "ajv": "^6.5.5", "har-schema": "^2.0.0" @@ -10169,17 +10372,17 @@ "json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha1-afaofZUTq4u4/mO9sJecRI5oRmA=" + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" }, "mime-db": { "version": "1.40.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", - "integrity": "sha1-plBX6ZjbCQ9zKmj2wnbTh9QSbDI=" + "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==" }, "mime-types": { "version": "2.1.24", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz", - "integrity": "sha1-tvjQs+lR77d97eyhlM/20W9nb4E=", + "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==", "requires": { "mime-db": "1.40.0" } @@ -10187,17 +10390,17 @@ "oauth-sign": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", - "integrity": "sha1-R6ewFrqmi1+g7PPe4IqFxnmsZFU=" + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" }, "psl": { "version": "1.1.32", "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.32.tgz", - "integrity": "sha1-PxMnF88vnBaXJLK2yvNzz2lBmNs=" + "integrity": "sha512-MHACAkHpihU/REGGPLj4sEfc/XKW2bheigvHO1dUqjaKigMp1C8+WLQYRGgeKFMsw5PMfegZcaN8IDXK/cD0+g==" }, "request": { "version": "2.88.0", "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", - "integrity": "sha1-nC/KT301tZLv5Xx/ClXoEFIST+8=", + "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", "requires": { "aws-sign2": "~0.7.0", "aws4": "^1.8.0", @@ -10224,7 +10427,7 @@ "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha1-mR7GnSluAxN0fVm9/St0XDX4go0=" + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, "semver": { "version": "5.3.0", @@ -10234,7 +10437,7 @@ "tough-cookie": { "version": "2.4.3", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", - "integrity": "sha1-U/Nto/R3g7CSWvoG/587FlKA94E=", + "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", "requires": { "psl": "^1.1.24", "punycode": "^1.4.1" @@ -10243,7 +10446,7 @@ "uuid": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", - "integrity": "sha1-G0r0lV6zB3xQHCOHL8ZROBFYcTE=" + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" } } }, @@ -10325,7 +10528,7 @@ "node-sass": { "version": "4.12.0", "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.12.0.tgz", - "integrity": "sha1-CRT1MZMjgBFKMMxfpPpjIzol8Bc=", + "integrity": "sha512-A1Iv4oN+Iel6EPv77/HddXErL2a+gZ4uBeZUy+a8O35CFYTXhgA8MgLCWBtwpGZdCvTvQ9d+bQxX/QC36GDPpQ==", "requires": { "async-foreach": "^0.1.3", "chalk": "^1.1.1", @@ -10349,7 +10552,7 @@ "ajv": { "version": "6.10.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.0.tgz", - "integrity": "sha1-kNDVRDnaWHzX6EO/twRfUL0ivfE=", + "integrity": "sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg==", "requires": { "fast-deep-equal": "^2.0.1", "fast-json-stable-stringify": "^2.0.0", @@ -10360,7 +10563,7 @@ "aws4": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", - "integrity": "sha1-8OAD2cqef1nHpQiUXXsu+aBKVC8=" + "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==" }, "cross-spawn": { "version": "3.0.1", @@ -10374,7 +10577,7 @@ "extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha1-+LETa0Bx+9jrFAr/hYsQGewpFfo=" + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" }, "fast-deep-equal": { "version": "2.0.1", @@ -10384,7 +10587,7 @@ "har-validator": { "version": "5.1.3", "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", - "integrity": "sha1-HvievT5JllV2de7ZiTEQ3DUPoIA=", + "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", "requires": { "ajv": "^6.5.5", "har-schema": "^2.0.0" @@ -10393,22 +10596,22 @@ "json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha1-afaofZUTq4u4/mO9sJecRI5oRmA=" + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" }, "lodash": { "version": "4.17.11", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", - "integrity": "sha1-s56mIp72B+zYniyN8SU2iRysm40=" + "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==" }, "mime-db": { "version": "1.40.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", - "integrity": "sha1-plBX6ZjbCQ9zKmj2wnbTh9QSbDI=" + "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==" }, "mime-types": { "version": "2.1.24", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz", - "integrity": "sha1-tvjQs+lR77d97eyhlM/20W9nb4E=", + "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==", "requires": { "mime-db": "1.40.0" } @@ -10421,17 +10624,17 @@ "oauth-sign": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", - "integrity": "sha1-R6ewFrqmi1+g7PPe4IqFxnmsZFU=" + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" }, "psl": { "version": "1.1.32", "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.32.tgz", - "integrity": "sha1-PxMnF88vnBaXJLK2yvNzz2lBmNs=" + "integrity": "sha512-MHACAkHpihU/REGGPLj4sEfc/XKW2bheigvHO1dUqjaKigMp1C8+WLQYRGgeKFMsw5PMfegZcaN8IDXK/cD0+g==" }, "request": { "version": "2.88.0", "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", - "integrity": "sha1-nC/KT301tZLv5Xx/ClXoEFIST+8=", + "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", "requires": { "aws-sign2": "~0.7.0", "aws4": "^1.8.0", @@ -10458,12 +10661,12 @@ "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha1-mR7GnSluAxN0fVm9/St0XDX4go0=" + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, "tough-cookie": { "version": "2.4.3", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", - "integrity": "sha1-U/Nto/R3g7CSWvoG/587FlKA94E=", + "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", "requires": { "psl": "^1.1.24", "punycode": "^1.4.1" @@ -10472,7 +10675,7 @@ "uuid": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", - "integrity": "sha1-G0r0lV6zB3xQHCOHL8ZROBFYcTE=" + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" } } }, @@ -13227,7 +13430,7 @@ "npmlog": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", - "integrity": "sha1-CKfyqL9zRgR3mp76StXMcXq7lUs=", + "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", "requires": { "are-we-there-yet": "~1.1.2", "console-control-strings": "~1.1.0", @@ -13497,7 +13700,7 @@ "osenv": { "version": "0.1.5", "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", - "integrity": "sha1-hc36+uso6Gd/QW4odZK18/SepBA=", + "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", "requires": { "os-homedir": "^1.0.0", "os-tmpdir": "^1.0.0" @@ -13730,7 +13933,7 @@ "path-parse": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", - "integrity": "sha1-1i27VnlAXXLEc37FhgDp3c8G0kw=" + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" }, "path-to-regexp": { "version": "0.1.7", @@ -14005,7 +14208,7 @@ "promise": { "version": "7.3.1", "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", - "integrity": "sha1-BktyYCsY+Q8pGSuLG8QY/9Hr078=", + "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", "requires": { "asap": "~2.0.3" } @@ -14360,6 +14563,15 @@ "scheduler": "^0.11.2" } }, + "react-event-timeline": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/react-event-timeline/-/react-event-timeline-1.6.3.tgz", + "integrity": "sha512-hMGhC9/Xx3sPF/TSlMCA13hZm/2c5CvBxbkDM7bQ4yq6VJ6AmhjqKPnU6/3nVmWUGpK3YqhHb95OiqulxVD3Eg==", + "dev": true, + "requires": { + "prop-types": "^15.6.0" + } + }, "react-fa": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/react-fa/-/react-fa-5.0.0.tgz", @@ -14600,7 +14812,7 @@ "react-spinners": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/react-spinners/-/react-spinners-0.5.4.tgz", - "integrity": "sha1-WBZvi/hMvwbdesytS5SleXz+qbk=", + "integrity": "sha512-jo7BE8prvnZNL7xNrQL16geVXH6LmaWrhvvXnmUwz2MhFO14bhlAdCLuKCwqmUJ37kGphH0C2CJRThwkjfpVzw==", "requires": { "@emotion/core": "^10.0.4", "prop-types": "^15.5.10", @@ -14648,7 +14860,7 @@ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", "requires": { - "js-tokens": "3.0.2" + "js-tokens": "^3.0.0 || ^4.0.0" } } } @@ -14751,7 +14963,7 @@ "recompose": { "version": "0.30.0", "resolved": "https://registry.npmjs.org/recompose/-/recompose-0.30.0.tgz", - "integrity": "sha1-gnc2QbOSfox9JKDYfWWu66GKq9A=", + "integrity": "sha512-ZTrzzUDa9AqUIhRk4KmVFihH0rapdCSMFXjhHbNrjAWxBuUD/guYlyysMnuHjlZC/KRiOKRtB4jf96yYSkKE8w==", "requires": { "@babel/runtime": "^7.0.0", "change-emitter": "^0.1.2", @@ -15047,7 +15259,7 @@ "resolve-pathname": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-2.2.0.tgz", - "integrity": "sha1-fpriHtgV/WOrGJre7mTcgx7vqHk=" + "integrity": "sha512-bAFz9ld18RzJfddgrO2e/0S2O81710++chRMUxHjXOYKF6jTAMrUNZrEZ1PvV0zlhfjidm08iRPdTLPno1FuRg==" }, "resolve-url": { "version": "0.2.1", @@ -15123,6 +15335,11 @@ "aproba": "^1.1.1" } }, + "rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha1-P4Yt+pGrdmsUiF700BEkv9oHT7Q=" + }, "rxjs": { "version": "6.3.3", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.3.3.tgz", @@ -15242,7 +15459,7 @@ "sass-loader": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-7.1.0.tgz", - "integrity": "sha1-Fv1ROMuLQkv4p1lSihly1yqtBp0=", + "integrity": "sha512-+G+BKGglmZM2GUSfT9TLuEp6tzehHPjAMoRRItOojWIqIGPloVCMhNIQuG639eJ+y033PaGTSjLaTHts8Kw79w==", "requires": { "clone-deep": "^2.0.1", "loader-utils": "^1.0.1", @@ -15255,12 +15472,12 @@ "big.js": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", - "integrity": "sha1-ZfCvOC9Xi83HQr2cKB6cstd2gyg=" + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==" }, "json5": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha1-d5+wAYYE+oVOrL9iUhgNg1Q+Pb4=", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", "requires": { "minimist": "^1.2.0" } @@ -15268,7 +15485,7 @@ "loader-utils": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.2.3.tgz", - "integrity": "sha1-H/XcaRHJ8KBiUxpMBLYJQGEIwsc=", + "integrity": "sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==", "requires": { "big.js": "^5.2.2", "emojis-list": "^2.0.0", @@ -15283,7 +15500,7 @@ "semver": { "version": "5.7.0", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", - "integrity": "sha1-eQp89v6lRZuslhELKbYEEtyP+Ws=" + "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==" } } }, @@ -15525,7 +15742,7 @@ "shallow-clone": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-1.0.0.tgz", - "integrity": "sha1-RIDNBuiC72iyrYij6lSDLixItXE=", + "integrity": "sha512-oeXreoKR/SyNJtRJMAKPDSvd28OqEwG4eR/xc856cRGBII7gX9lvAqDxusPm0846z/w/hWYjI1NpKwJ00NHzRA==", "requires": { "is-extendable": "^0.1.1", "kind-of": "^5.0.0", @@ -15535,7 +15752,7 @@ "kind-of": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha1-cpyR4thXt6QZofmqZWhcTDP1hF0=" + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==" } } }, @@ -16027,7 +16244,7 @@ "stdout-stream": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/stdout-stream/-/stdout-stream-1.4.1.tgz", - "integrity": "sha1-WsF0zdXNcmEEqgwLK9g4FdjVNd4=", + "integrity": "sha512-j4emi03KXqJWcIeF8eIXkjMFN1Cmb8gUlDYGeBALLPo5qdyTfA9bOtl8m33lRoC+vFMkP3gl0WsDr6+gzxbbTA==", "requires": { "readable-stream": "^2.0.1" }, @@ -16040,12 +16257,12 @@ "process-nextick-args": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", - "integrity": "sha1-o31zL0JxtKsa0HDTVQjoKQeI/6o=" + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" }, "readable-stream": { "version": "2.3.6", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha1-sRwn2IuP8fvgcGQ8+UsMea4bCq8=", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -16059,7 +16276,7 @@ "string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha1-nPFhG6YmhdcDCunkujQUnDrwP8g=", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "requires": { "safe-buffer": "~5.1.0" } @@ -16397,7 +16614,7 @@ "tar": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.2.tgz", - "integrity": "sha1-DKiEhWLHKZuLRG/2pNYM27I+3EA=", + "integrity": "sha512-FCEhQ/4rE1zYv9rYXJw/msRqsnmlje5jHP6huWeBZ704jUTy02c5AZyWujpMR1ax6mVw9NyJMfuK2CMDWVIfgA==", "requires": { "block-stream": "*", "fstream": "^1.0.12", @@ -16588,7 +16805,7 @@ "true-case-path": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/true-case-path/-/true-case-path-1.0.3.tgz", - "integrity": "sha1-+BO1qMhrQNpZYGcisUTjIleZ9H0=", + "integrity": "sha512-m6s2OdQe5wgpFMC+pAJ+q9djG82O2jcHPOI6RNg1yy9rCYR+WD6Nbpl32fDpfC56nirdRy+opFa/Vk7HYhqaew==", "requires": { "glob": "^7.1.2" } @@ -17069,7 +17286,7 @@ "value-equal": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/value-equal/-/value-equal-0.4.0.tgz", - "integrity": "sha1-xb3S9U7gk8BIOdcc4uR1imiQq8c=" + "integrity": "sha512-x+cYdNnaA3CxvMaTX0INdTCN8m8aF2uY9BvEqmxuYp8bL09cs/kWVQPVGcA35fMktdOsP69IgU7wFj/61dJHEw==" }, "vary": { "version": "1.1.2", @@ -19045,8 +19262,7 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "aproba": { "version": "1.2.0", @@ -19089,8 +19305,7 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "concat-map": { "version": "0.0.1", @@ -19101,8 +19316,7 @@ "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "core-util-is": { "version": "1.0.2", @@ -19219,8 +19433,7 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "ini": { "version": "1.3.5", @@ -19232,7 +19445,6 @@ "version": "1.0.0", "bundled": true, "dev": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -19249,20 +19461,18 @@ "dev": true, "optional": true, "requires": { - "brace-expansion": "1.1.11" + "brace-expansion": "^1.1.7" } }, "minimist": { "version": "0.0.8", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "minipass": { "version": "2.2.4", "bundled": true, "dev": true, - "optional": true, "requires": { "safe-buffer": "^5.1.1", "yallist": "^3.0.0" @@ -19281,7 +19491,6 @@ "version": "0.5.1", "bundled": true, "dev": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -19362,8 +19571,7 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "object-assign": { "version": "4.1.1", @@ -19375,7 +19583,6 @@ "version": "1.4.0", "bundled": true, "dev": true, - "optional": true, "requires": { "wrappy": "1" } @@ -19461,8 +19668,7 @@ "safe-buffer": { "version": "5.1.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "safer-buffer": { "version": "2.1.2", @@ -19498,7 +19704,6 @@ "version": "1.0.2", "bundled": true, "dev": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -19518,7 +19723,6 @@ "version": "3.0.1", "bundled": true, "dev": true, - "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -19562,14 +19766,12 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "yallist": { "version": "3.0.2", "bundled": true, - "dev": true, - "optional": true + "dev": true } } }, @@ -19979,7 +20181,7 @@ "whatwg-fetch": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.0.0.tgz", - "integrity": "sha1-/IBORYzEYACbGiuWa8iBfSV4rvs=" + "integrity": "sha512-9GSJUgz1D4MfyKU7KRqwOjXCXTqWdFNvEr7eUBYchQiVc744mqK/MzXPNR2WsPkmkOa4ywfg8C2n8h+13Bey1Q==" }, "which": { "version": "1.3.0", @@ -19998,7 +20200,7 @@ "wide-align": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", - "integrity": "sha1-rgdOa9wMFKQx6ATmJFScYzsABFc=", + "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", "requires": { "string-width": "^1.0.2 || 2" } diff --git a/monkey/monkey_island/cc/ui/package.json b/monkey/monkey_island/cc/ui/package.json index a76541e36..4da085836 100644 --- a/monkey/monkey_island/cc/ui/package.json +++ b/monkey/monkey_island/cc/ui/package.json @@ -55,6 +55,7 @@ "null-loader": "^0.1.1", "phantomjs-prebuilt": "^2.1.16", "react-addons-test-utils": "^15.6.2", + "react-event-timeline": "^1.6.3", "react-hot-loader": "^4.3.11", "rimraf": "^2.6.2", "style-loader": "^0.22.1", @@ -64,12 +65,15 @@ "webpack-dev-server": "^3.1.9" }, "dependencies": { + "@emotion/core": "^10.0.10", "@kunukn/react-collapse": "^1.0.5", - "classnames": "^2.2.6", "bootstrap": "3.4.1", + "classnames": "^2.2.6", "core-js": "^2.5.7", + "d3": "^5.11.0", "downloadjs": "^1.4.7", "fetch": "^1.1.0", + "file-saver": "^2.0.2", "filepond": "^4.2.0", "js-file-download": "^0.4.4", "json-loader": "^0.5.7", @@ -84,6 +88,7 @@ "react-bootstrap": "^0.32.4", "react-copy-to-clipboard": "^5.0.1", "react-data-components": "^1.2.0", + "react-desktop-notification": "^1.0.9", "react-dimensions": "^1.3.0", "react-dom": "^16.5.2", "react-fa": "^5.0.0", @@ -93,14 +98,13 @@ "react-jsonschema-form": "^1.0.5", "react-redux": "^5.1.1", "react-router-dom": "^4.3.1", + "react-spinners": "^0.5.4", "react-table": "^6.8.6", "react-toggle": "^4.0.1", "react-tooltip-lite": "^1.9.1", "redux": "^4.0.0", "sass-loader": "^7.1.0", "sha3": "^2.0.0", - "react-spinners": "^0.5.4", - "@emotion/core": "^10.0.10", - "react-desktop-notification": "^1.0.9" + "pluralize": "^7.0.0" } } diff --git a/monkey/monkey_island/cc/ui/src/components/Main.js b/monkey/monkey_island/cc/ui/src/components/Main.js index 619d5e922..09038292e 100644 --- a/monkey/monkey_island/cc/ui/src/components/Main.js +++ b/monkey/monkey_island/cc/ui/src/components/Main.js @@ -7,10 +7,10 @@ import RunServerPage from 'components/pages/RunServerPage'; import ConfigurePage from 'components/pages/ConfigurePage'; import RunMonkeyPage from 'components/pages/RunMonkeyPage'; import MapPage from 'components/pages/MapPage'; -import PassTheHashMapPage from 'components/pages/PassTheHashMapPage'; import TelemetryPage from 'components/pages/TelemetryPage'; import StartOverPage from 'components/pages/StartOverPage'; import ReportPage from 'components/pages/ReportPage'; +import ZeroTrustReportPage from 'components/pages/ZeroTrustReportPage'; import LicensePage from 'components/pages/LicensePage'; import AuthComponent from 'components/AuthComponent'; import LoginPageComponent from 'components/pages/LoginPage'; @@ -29,6 +29,8 @@ let infectionMonkeyImage = require('../images/infection-monkey.svg'); let guardicoreLogoImage = require('../images/guardicore-logo.png'); let notificationIcon = require('../images/notification-logo-512x512.png'); +const reportZeroTrustRoute = '/report/zero_trust'; + class AppComponent extends AuthComponent { updateStatus = () => { this.auth.loggedIn() @@ -148,7 +150,7 @@ class AppComponent extends AuthComponent {
  • - + 4. Security Report {this.state.completedSteps.report_done ? @@ -156,6 +158,15 @@ class AppComponent extends AuthComponent { : ''}
  • +
  • + + 5. + Zero Trust Report + {this.state.completedSteps.report_done ? + + : ''} + +
  • @@ -190,7 +201,8 @@ class AppComponent extends AuthComponent { {this.renderRoute('/infection/map', )} {this.renderRoute('/infection/telemetry', )} {this.renderRoute('/start-over', )} - {this.renderRoute('/report', )} + {this.renderRoute('/report/security', )} + {this.renderRoute(reportZeroTrustRoute, )} {this.renderRoute('/license', )} @@ -200,10 +212,11 @@ class AppComponent extends AuthComponent { } showInfectionDoneNotification() { - if (this.state.completedSteps.infection_done) { - let hostname = window.location.hostname; - let url = `https://${hostname}:5000/report`; - console.log("Trying to show notification. URL: " + url + " | icon: " + notificationIcon); + if (this.shouldShowNotification()) { + const hostname = window.location.hostname; + const port = window.location.port; + const protocol = window.location.protocol; + const url = `${protocol}//${hostname}:${port}${reportZeroTrustRoute}`; Notifier.start( "Monkey Island", @@ -212,6 +225,11 @@ class AppComponent extends AuthComponent { notificationIcon); } } + + shouldShowNotification() { + // No need to show the notification to redirect to the report if we're already in the report page + return (this.state.completedSteps.infection_done && !window.location.pathname.startsWith("/report")); + } } AppComponent.defaultProps = {}; diff --git a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1003.js b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1003.js index 07fd4a400..24d742c14 100644 --- a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1003.js +++ b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1003.js @@ -1,7 +1,7 @@ import React from 'react'; import '../../../styles/Collapse.scss' -import '../../report-components/StolenPasswords' -import StolenPasswordsComponent from "../../report-components/StolenPasswords"; +import '../../report-components/security/StolenPasswords' +import StolenPasswordsComponent from "../../report-components/security/StolenPasswords"; import {ScanStatus} from "./Helpers" diff --git a/monkey/monkey_island/cc/ui/src/components/pages/ReportPage.js b/monkey/monkey_island/cc/ui/src/components/pages/ReportPage.js index 207ae8699..68ba84aa6 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/ReportPage.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/ReportPage.js @@ -1,20 +1,27 @@ -import React from 'react'; +import React, {Fragment} from 'react'; import {Button, Col} from 'react-bootstrap'; -import BreachedServers from 'components/report-components/BreachedServers'; -import ScannedServers from 'components/report-components/ScannedServers'; -import PostBreach from 'components/report-components/PostBreach'; +import BreachedServers from 'components/report-components/security/BreachedServers'; +import ScannedServers from 'components/report-components/security/ScannedServers'; +import PostBreach from 'components/report-components/security/PostBreach'; import {ReactiveGraph} from 'components/reactive-graph/ReactiveGraph'; import {edgeGroupToColor, options} from 'components/map/MapOptions'; -import StolenPasswords from 'components/report-components/StolenPasswords'; -import CollapsibleWellComponent from 'components/report-components/CollapsibleWell'; +import StolenPasswords from 'components/report-components/security/StolenPasswords'; +import CollapsibleWellComponent from 'components/report-components/security/CollapsibleWell'; 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"; +import StrongUsers from "components/report-components/security/StrongUsers"; +import AttackReport from "components/report-components/security/AttackReport"; +import ReportHeader, {ReportTypes} from "../report-components/common/ReportHeader"; +import MonkeysStillAliveWarning from "../report-components/common/MonkeysStillAliveWarning"; +import ReportLoader from "../report-components/common/ReportLoader"; +import MustRunMonkeyWarning from "../report-components/common/MustRunMonkeyWarning"; +import SecurityIssuesGlance from "../report-components/common/SecurityIssuesGlance"; +import PrintReportButton from "../report-components/common/PrintReportButton"; +import {extractExecutionStatusFromServerResponse} from "../report-components/common/ExecutionStatus"; let guardicoreLogoImage = require('../../images/guardicore-logo.png'); -let monkeyLogoImage = require('../../images/monkey-icon.svg'); + class ReportPageComponent extends AuthComponent { @@ -66,18 +73,11 @@ class ReportPageComponent extends AuthComponent { render() { let content; - if (Object.keys(this.state.report).length === 0) { - if (this.state.runStarted) { - content = (

    Generating Report...

    ); - } else { - content = -

    - - You have to run a monkey before generating a report! -

    ; - } - } else { + + if (this.state.runStarted) { content = this.generateReportContent(); + } else { + content = ; } return ( @@ -90,15 +90,15 @@ class ReportPageComponent extends AuthComponent { ); } + stillLoadingDataFromServer() { + return Object.keys(this.state.report).length === 0; + } + 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'] - }); + this.setState(extractExecutionStatusFromServerResponse(res)); return res; }); }; @@ -117,7 +117,7 @@ class ReportPageComponent extends AuthComponent { getReportFromServer(res) { if (res['completed_steps']['run_monkey']) { - this.authFetch('/api/report') + this.authFetch('/api/report/security') .then(res => res.json()) .then(res => { this.setState({ @@ -128,49 +128,36 @@ class ReportPageComponent extends AuthComponent { } generateReportContent() { + let content; + + if (this.stillLoadingDataFromServer()) { + content = ; + } else { + content = +
    + {this.generateReportOverviewSection()} + {this.generateReportFindingsSection()} + {this.generateReportRecommendationsSection()} + {this.generateReportGlanceSection()} + {this.generateAttackSection()} + {this.generateReportFooter()} +
    ; + } + return ( -
    -
    - + +
    + {print();}} />
    - {this.generateReportHeader()} +
    - {this.generateReportOverviewSection()} - {this.generateReportFindingsSection()} - {this.generateReportRecommendationsSection()} - {this.generateReportGlanceSection()} - {this.generateAttackSection()} - {this.generateReportFooter()} + {content}
    -
    - +
    + {print();}} />
    -
    - ); - } - - generateReportHeader() { - return ( - +
    ); } @@ -180,27 +167,8 @@ class ReportPageComponent extends AuthComponent {

    Overview

    - { - this.state.report.glance.exploited.length > 0 ? - (

    - - Critical security issues were detected! -

    ) : - (

    - - No critical security issues were detected. -

    ) - } - { - this.state.allMonkeysAreDead ? - '' - : - (

    - - Some monkeys are still running. To get the best report it's best to wait for all of them to finish - running. -

    ) - } + 0}/> + { this.state.report.glance.exploited.length > 0 ? '' diff --git a/monkey/monkey_island/cc/ui/src/components/pages/ZeroTrustReportPage.js b/monkey/monkey_island/cc/ui/src/components/pages/ZeroTrustReportPage.js new file mode 100755 index 000000000..a0b92d9bd --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/pages/ZeroTrustReportPage.js @@ -0,0 +1,134 @@ +import React, {Fragment} from 'react'; +import {Col} from 'react-bootstrap'; +import AuthComponent from '../AuthComponent'; +import ReportHeader, {ReportTypes} from "../report-components/common/ReportHeader"; +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 SummarySection from "../report-components/zerotrust/SummarySection"; +import FindingsSection from "../report-components/zerotrust/FindingsSection"; +import PrinciplesSection from "../report-components/zerotrust/PrinciplesSection"; + +class ZeroTrustReportPageComponent extends AuthComponent { + + constructor(props) { + super(props); + + this.state = { + allMonkeysAreDead: false, + runStarted: true + }; + } + + componentDidMount() { + this.updatePageState(); + const refreshInterval = setInterval(this.updatePageState, 8000); + this.setState( + {refreshDataIntervalHandler: refreshInterval} + ) + } + + componentWillUnmount() { + clearInterval(this.state.refreshDataIntervalHandler); + } + + updateMonkeysRunning = () => { + return this.authFetch('/api') + .then(res => res.json()) + .then(res => { + this.setState(extractExecutionStatusFromServerResponse(res)); + return res; + }); + }; + + updatePageState = () => { + this.updateMonkeysRunning().then(res => this.getZeroTrustReportFromServer(res)); + }; + + render() { + let content; + if (this.state.runStarted) { + content = this.generateReportContent(); + } else { + content = ; + } + + return ( + +

    5. Zero Trust Report

    +
    + {content} +
    + + ); + } + + generateReportContent() { + let content; + + if (this.stillLoadingDataFromServer()) { + content = ; + } else { + content =
    + + + +
    ; + } + + return ( + +
    + { + print(); + }}/> +
    +
    + +
    + {content} +
    +
    + { + print(); + }}/> +
    +
    + ) + } + + stillLoadingDataFromServer() { + return typeof this.state.findings === "undefined" + || typeof this.state.pillars === "undefined" + || typeof this.state.principles === "undefined"; + } + + getZeroTrustReportFromServer() { + let res; + this.authFetch('/api/report/zero_trust/findings') + .then(res => res.json()) + .then(res => { + this.setState({ + findings: res + }); + }); + this.authFetch('/api/report/zero_trust/principles') + .then(res => res.json()) + .then(res => { + this.setState({ + principles: res + }); + }); + this.authFetch('/api/report/zero_trust/pillars') + .then(res => res.json()) + .then(res => { + this.setState({ + pillars: res + }); + }); + } +} + +export default ZeroTrustReportPageComponent; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/common/ExecutionStatus.js b/monkey/monkey_island/cc/ui/src/components/report-components/common/ExecutionStatus.js new file mode 100644 index 000000000..840e570d7 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/common/ExecutionStatus.js @@ -0,0 +1,6 @@ +export function extractExecutionStatusFromServerResponse(res) { + return { + allMonkeysAreDead: (!res['completed_steps']['run_monkey']) || (res['completed_steps']['infection_done']), + runStarted: res['completed_steps']['run_monkey'] + }; +} diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/common/MonkeysStillAliveWarning.js b/monkey/monkey_island/cc/ui/src/components/report-components/common/MonkeysStillAliveWarning.js new file mode 100644 index 000000000..7b72570fa --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/common/MonkeysStillAliveWarning.js @@ -0,0 +1,21 @@ +import React, {Component} from "react"; +import * as PropTypes from "prop-types"; + +export default class MonkeysStillAliveWarning extends Component { + render() { + return
    + { + this.props.allMonkeysAreDead ? + '' + : + (

    + + Some monkeys are still running. To get the best report it's best to wait for all of them to finish + running. +

    ) + } +
    + } +} + +MonkeysStillAliveWarning.propTypes = {allMonkeysAreDead: PropTypes.bool}; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/common/MustRunMonkeyWarning.js b/monkey/monkey_island/cc/ui/src/components/report-components/common/MustRunMonkeyWarning.js new file mode 100644 index 000000000..f1d23e302 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/common/MustRunMonkeyWarning.js @@ -0,0 +1,11 @@ +import React, {Component} from "react"; +import {NavLink} from "react-router-dom"; + +export default class MustRunMonkeyWarning extends Component { + render() { + return

    + + You have to run a monkey before generating a report! +

    + } +} diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/common/PaginatedTable.js b/monkey/monkey_island/cc/ui/src/components/report-components/common/PaginatedTable.js new file mode 100644 index 000000000..5bc6183fd --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/common/PaginatedTable.js @@ -0,0 +1,36 @@ +import React, {Component} from "react"; +import ReactTable from "react-table"; +import * as PropTypes from "prop-types"; + +class PaginatedTable extends Component { + render() { + if (this.props.data.length > 0) { + let defaultPageSize = this.props.data.length > this.props.pageSize ? this.props.pageSize : this.props.data.length; + let showPagination = this.props.data.length > this.props.pageSize; + + return ( +
    + +
    + ); + } + else { + return ( +
    + ); + } + } +} + +export default PaginatedTable; + +PaginatedTable.propTypes = { + data: PropTypes.array, + columns: PropTypes.array, + pageSize: PropTypes.number, +}; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/common/PrintReportButton.js b/monkey/monkey_island/cc/ui/src/components/report-components/common/PrintReportButton.js new file mode 100644 index 000000000..1a692bd68 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/common/PrintReportButton.js @@ -0,0 +1,14 @@ +import React, {Component} from "react"; +import {Button} from "react-bootstrap"; +import * as PropTypes from "prop-types"; + +export default class PrintReportButton extends Component { + render() { + return
    + +
    + } +} + +PrintReportButton.propTypes = {onClick: PropTypes.func}; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/common/ReportHeader.js b/monkey/monkey_island/cc/ui/src/components/report-components/common/ReportHeader.js new file mode 100644 index 000000000..44d470f7e --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/common/ReportHeader.js @@ -0,0 +1,45 @@ +import React, {Component} from "react"; +import {Col} from "react-bootstrap"; +import * as PropTypes from "prop-types"; + +let monkeyLogoImage = require('../../../images/monkey-icon.svg'); + +export const ReportTypes = { + zeroTrust: "Zero Trust", + security: "Security", + null: "" +}; + +export class ReportHeader extends Component { + report_type; + + render() { + return + } +} + +export default ReportHeader; + +ReportHeader.propTypes = { + report_type: PropTypes.string, +}; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/common/ReportLoader.js b/monkey/monkey_island/cc/ui/src/components/report-components/common/ReportLoader.js new file mode 100644 index 000000000..e389f7532 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/common/ReportLoader.js @@ -0,0 +1,28 @@ +import {css} from "@emotion/core"; +import React, {Component} from "react"; +import {GridLoader} from "react-spinners"; +import * as PropTypes from "prop-types"; + +const loading_css_override = css` + display: block; + margin-right: auto; + margin-left: auto; +`; + + +export default class ReportLoader extends Component { + render() { + return
    +

    Generating Report...

    + +
    + } +} + +ReportLoader.propTypes = {loading: PropTypes.bool}; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/common/SecurityIssuesGlance.js b/monkey/monkey_island/cc/ui/src/components/report-components/common/SecurityIssuesGlance.js new file mode 100644 index 000000000..41a45edad --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/common/SecurityIssuesGlance.js @@ -0,0 +1,22 @@ +import React, {Component, Fragment} from "react"; +import * as PropTypes from "prop-types"; + +export default class SecurityIssuesGlance extends Component { + render() { + return + { + this.props.issuesFound ? + (

    + + Critical security issues were detected! +

    ) : + (

    + + No critical security issues were detected. +

    ) + } +
    + } +} + +SecurityIssuesGlance.propTypes = {issuesFound: PropTypes.bool}; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/AttackReport.js b/monkey/monkey_island/cc/ui/src/components/report-components/security/AttackReport.js similarity index 75% rename from monkey/monkey_island/cc/ui/src/components/report-components/AttackReport.js rename to monkey/monkey_island/cc/ui/src/components/report-components/security/AttackReport.js index 10381fa5e..13f9cd92e 100644 --- a/monkey/monkey_island/cc/ui/src/components/report-components/AttackReport.js +++ b/monkey/monkey_island/cc/ui/src/components/report-components/security/AttackReport.js @@ -2,34 +2,36 @@ import React from 'react'; import {Col} from 'react-bootstrap'; import {ReactiveGraph} from 'components/reactive-graph/ReactiveGraph'; import {edgeGroupToColor, options} from 'components/map/MapOptions'; -import '../../styles/Collapse.scss'; -import AuthComponent from '../AuthComponent'; -import {ScanStatus} from "../attack/techniques/Helpers"; +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'; -import T1075 from "../attack/techniques/T1075"; -import T1003 from "../attack/techniques/T1003"; -import T1059 from "../attack/techniques/T1059"; -import T1086 from "../attack/techniques/T1086"; -import T1082 from "../attack/techniques/T1082"; -import T1145 from "../attack/techniques/T1145"; -import T1105 from "../attack/techniques/T1105"; -import T1107 from "../attack/techniques/T1107"; -import T1065 from "../attack/techniques/T1065"; -import T1035 from "../attack/techniques/T1035"; -import T1129 from "../attack/techniques/T1129"; -import T1106 from "../attack/techniques/T1106"; -import T1188 from "../attack/techniques/T1188"; -import T1090 from "../attack/techniques/T1090"; -import T1041 from "../attack/techniques/T1041"; -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 T1210 from '../../attack/techniques/T1210'; +import T1197 from '../../attack/techniques/T1197'; +import T1110 from '../../attack/techniques/T1110'; +import T1075 from "../../attack/techniques/T1075"; +import T1003 from "../../attack/techniques/T1003"; +import T1059 from "../../attack/techniques/T1059"; +import T1086 from "../../attack/techniques/T1086"; +import T1082 from "../../attack/techniques/T1082"; +import T1145 from "../../attack/techniques/T1145"; +import T1105 from "../../attack/techniques/T1105"; +import T1107 from "../../attack/techniques/T1107"; +import T1065 from "../../attack/techniques/T1065"; +import T1035 from "../../attack/techniques/T1035"; +import T1129 from "../../attack/techniques/T1129"; +import T1106 from "../../attack/techniques/T1106"; +import T1188 from "../../attack/techniques/T1188"; +import T1090 from "../../attack/techniques/T1090"; +import T1041 from "../../attack/techniques/T1041"; +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 = { 'T1210': T1210, @@ -80,11 +82,7 @@ class AttackReportPageComponent extends AuthComponent { 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'] - }); + this.setState(extractExecutionStatusFromServerResponse(res)); return res; }); }; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/BreachedServers.js b/monkey/monkey_island/cc/ui/src/components/report-components/security/BreachedServers.js similarity index 100% rename from monkey/monkey_island/cc/ui/src/components/report-components/BreachedServers.js rename to monkey/monkey_island/cc/ui/src/components/report-components/security/BreachedServers.js diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/CollapsibleWell.js b/monkey/monkey_island/cc/ui/src/components/report-components/security/CollapsibleWell.js similarity index 100% rename from monkey/monkey_island/cc/ui/src/components/report-components/CollapsibleWell.js rename to monkey/monkey_island/cc/ui/src/components/report-components/security/CollapsibleWell.js diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/PostBreach.js b/monkey/monkey_island/cc/ui/src/components/report-components/security/PostBreach.js similarity index 100% rename from monkey/monkey_island/cc/ui/src/components/report-components/PostBreach.js rename to monkey/monkey_island/cc/ui/src/components/report-components/security/PostBreach.js diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/ScannedServers.js b/monkey/monkey_island/cc/ui/src/components/report-components/security/ScannedServers.js similarity index 100% rename from monkey/monkey_island/cc/ui/src/components/report-components/ScannedServers.js rename to monkey/monkey_island/cc/ui/src/components/report-components/security/ScannedServers.js diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/StolenPasswords.js b/monkey/monkey_island/cc/ui/src/components/report-components/security/StolenPasswords.js similarity index 100% rename from monkey/monkey_island/cc/ui/src/components/report-components/StolenPasswords.js rename to monkey/monkey_island/cc/ui/src/components/report-components/security/StolenPasswords.js diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/StrongUsers.js b/monkey/monkey_island/cc/ui/src/components/report-components/security/StrongUsers.js similarity index 100% rename from monkey/monkey_island/cc/ui/src/components/report-components/StrongUsers.js rename to monkey/monkey_island/cc/ui/src/components/report-components/security/StrongUsers.js diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/EventsButton.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/EventsButton.js new file mode 100644 index 000000000..761ff94a9 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/EventsButton.js @@ -0,0 +1,43 @@ +import React, {Component, Fragment} from "react"; +import EventsModal from "./EventsModal"; +import {Badge, Button} from "react-bootstrap"; +import * as PropTypes from "prop-types"; + +export default class EventsButton extends Component { + constructor(props) { + super(props); + this.state = { + isShow: false + } + } + + hide = () => { + this.setState({isShow: false}); + }; + + show = () => { + this.setState({isShow: true}); + }; + + render() { + return + +
    + +
    +
    ; + } + + createEventsAmountBadge() { + const eventsAmountBadgeContent = this.props.events.length > 9 ? "9+" : this.props.events.length; + return {eventsAmountBadgeContent}; + } +} + +EventsButton.propTypes = { + events: PropTypes.array, + exportFilename: PropTypes.string, +}; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/EventsModal.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/EventsModal.js new file mode 100644 index 000000000..a7f2fe41c --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/EventsModal.js @@ -0,0 +1,51 @@ +import React, {Component} from "react"; +import {Badge, Modal} from "react-bootstrap"; +import EventsTimeline from "./EventsTimeline"; +import * as PropTypes from "prop-types"; +import saveJsonToFile from "../../utils/SaveJsonToFile"; +import EventsModalButtons from "./EventsModalButtons"; +import Pluralize from 'pluralize' +import {statusToLabelType} from "./StatusLabel"; + +export default class EventsModal extends Component { + constructor(props) { + super(props); + } + + render() { + return ( +
    + this.props.hideCallback()}> + +

    +
    Events
    +

    +
    +

    + There {Pluralize('is', this.props.events.length)} {

    {this.props.events.length}
    } {Pluralize('event', this.props.events.length)} associated with this finding. +

    + {this.props.events.length > 5 ? this.renderButtons() : null} + + {this.renderButtons()} +
    +
    +
    + ); + } + + renderButtons() { + return this.props.hideCallback()} + onClickExport={() => { + const dataToSave = this.props.events; + const filename = this.props.exportFilename; + saveJsonToFile(dataToSave, filename); + }}/>; + } +} + +EventsModal.propTypes = { + showEvents: PropTypes.bool, + events: PropTypes.array, + hideCallback: PropTypes.func, +}; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/EventsModalButtons.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/EventsModalButtons.js new file mode 100644 index 000000000..962c54893 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/EventsModalButtons.js @@ -0,0 +1,20 @@ +import React, {Component} from "react"; +import ExportEventsButton from "./ExportEventsButton"; +import * as PropTypes from "prop-types"; + +export default class EventsModalButtons extends Component { + render() { + return
    + + +
    + } +} + +EventsModalButtons.propTypes = { + onClickClose: PropTypes.func, + onClickExport: PropTypes.func +}; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/EventsTimeline.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/EventsTimeline.js new file mode 100644 index 000000000..b7fb90811 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/EventsTimeline.js @@ -0,0 +1,36 @@ +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": monkeyLocalIcon, + "monkey_network": monkeyNetworkIcon, +}; + +export default class EventsTimeline extends Component { + render() { + return ( +
    + + { + this.props.events.map((event, index) => { + const event_time = new Date(event.timestamp['$date']).toString(); + return (}> + {event.message} + ) + }) + } + +
    + ); + } +} + +EventsTimeline.propTypes = {events: PropTypes.array}; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/ExportEventsButton.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/ExportEventsButton.js new file mode 100644 index 000000000..bb6fc6c45 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/ExportEventsButton.js @@ -0,0 +1,15 @@ +import React, {Component} from "react"; +import {Button} from "react-bootstrap"; +import * as PropTypes from "prop-types"; + +export default class ExportEventsButton extends Component { + render() { + return + } +} + +ExportEventsButton.propTypes = {onClick: PropTypes.func}; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/FindingsSection.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/FindingsSection.js new file mode 100644 index 000000000..95b9d0389 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/FindingsSection.js @@ -0,0 +1,63 @@ +import React, {Component, Fragment} from "react"; +import PillarLabel from "./PillarLabel"; +import EventsButton from "./EventsButton"; +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 ( +
    +

    Findings

    +

    + 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. +

    + + + + +
    + ); + } + + getFilteredFindings(statusToFilter) { + const findings = this.props.findings.map((finding) => { + // Deep copy + const newFinding = JSON.parse(JSON.stringify(finding)); + if (newFinding.status === statusToFilter) { + newFinding.pillars = newFinding.pillars.map((pillar) => { + return {name: pillar, status: this.props.pillarsToStatuses[pillar]} + }); + return newFinding; + } + }); + // Filter nulls out of the list + return findings.filter(function (el) { + return el != null; + }); + } +} + + +export default FindingsSection; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/FindingsTable.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/FindingsTable.js new file mode 100644 index 000000000..acff1df89 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/FindingsTable.js @@ -0,0 +1,53 @@ +import React, {Component, Fragment} from "react"; +import StatusLabel from "./StatusLabel"; +import PaginatedTable from "../common/PaginatedTable"; +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: [ + { + Header: 'Finding', accessor: 'test', + style: {'whiteSpace': 'unset'} // This enables word wrap + }, + + { + Header: 'Events', id: "events", + accessor: x => { + return ; + }, + maxWidth: EVENTS_COLUMN_MAX_WIDTH, + }, + + { + Header: 'Pillars', id: "pillars", + accessor: x => { + const pillars = x.pillars; + const pillarLabels = pillars.map((pillar) => + + ); + return
    {pillarLabels}
    ; + }, + maxWidth: PILLARS_COLUMN_MAX_WIDTH, + style: {'whiteSpace': 'unset'} + }, + ] + } +]; + + +export class FindingsTable extends Component { + render() { + return +

    { + } tests' findings

    + +
    + } +} + +FindingsTable.propTypes = {data: PropTypes.array, status: PropTypes.string}; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/PillarLabel.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/PillarLabel.js new file mode 100644 index 000000000..51c5ca380 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/PillarLabel.js @@ -0,0 +1,25 @@ +import React, {Component} from "react"; +import {statusToLabelType} from "./StatusLabel"; +import * as PropTypes from "prop-types"; + +const pillarToIcon = { + "Data": "fa fa-database", + "People": "fa fa-user", + "Networks": "fa fa-wifi", + "Workloads": "fa fa-cloud", + "Devices": "fa fa-laptop", + "Visibility & Analytics": "fa fa-eye-slash", + "Automation & Orchestration": "fa fa-cogs", +}; + +export default class PillarLabel extends Component { + render() { + const className = "label " + statusToLabelType[this.props.status]; + return
    {this.props.pillar}
    + } +} + +PillarLabel.propTypes = { + status: PropTypes.string, + pillar: PropTypes.string, +}; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/PillarOverview.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/PillarOverview.js new file mode 100644 index 000000000..7cefcab61 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/PillarOverview.js @@ -0,0 +1,17 @@ +import React, {Component} from "react"; +import * as PropTypes from "prop-types"; +import ResponsiveVennDiagram from "./venn-components/ResponsiveVennDiagram"; + +class PillarOverview extends Component { + render() { + return (
    + +
    ); + } +} + +export default PillarOverview; + +PillarOverview.propTypes = { + grades: PropTypes.array, +}; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/PrinciplesSection.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/PrinciplesSection.js new file mode 100644 index 000000000..bb957d42d --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/PrinciplesSection.js @@ -0,0 +1,31 @@ +import React, {Component} from "react"; +import SinglePillarPrinciplesStatus from "./SinglePillarPrinciplesStatus"; +import * as PropTypes from "prop-types"; + +export default class PrinciplesSection extends Component { + render() { + return
    +

    Test Results

    +

    + The + Zero Trust eXtended (ZTX) framework + is composed of 7 pillars. Each pillar is built of + several guiding principles tested by the Infection Monkey. +

    + { + Object.keys(this.props.principles).map((pillar) => + + ) + } +
    + } +} + +PrinciplesSection.propTypes = { + principles: PropTypes.object, + pillarsToStatuses: PropTypes.object +}; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/PrinciplesStatusTable.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/PrinciplesStatusTable.js new file mode 100644 index 000000000..b50ee0c28 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/PrinciplesStatusTable.js @@ -0,0 +1,71 @@ +import React, {Fragment} from "react"; +import PaginatedTable from "../common/PaginatedTable"; +import AuthComponent from "../../AuthComponent"; +import StatusLabel from "./StatusLabel"; +import * as PropTypes from "prop-types"; +import {ZeroTrustStatuses} from "./ZeroTrustPillars"; + + +const MAX_WIDTH_STATUS_COLUMN = 80; +const columns = [ + { + columns: [ + { Header: 'Status', id: 'status', + accessor: x => { + return ; + }, + maxWidth: MAX_WIDTH_STATUS_COLUMN + }, + { Header: 'Zero Trust Principle', accessor: 'principle', + style: {'whiteSpace': 'unset'} // This enables word wrap + }, + { Header: 'Monkey Tests', id: 'tests', + style: {'whiteSpace': 'unset'}, // This enables word wrap + accessor: x => { + return ; + } + } + ] + } +]; + +class TestsStatus extends AuthComponent { + render() { + return ( + + {this.getFilteredTestsByStatusIfAny(ZeroTrustStatuses.failed)} + {this.getFilteredTestsByStatusIfAny(ZeroTrustStatuses.verify)} + {this.getFilteredTestsByStatusIfAny(ZeroTrustStatuses.passed)} + {this.getFilteredTestsByStatusIfAny(ZeroTrustStatuses.unexecuted)} + + ); + } + + getFilteredTestsByStatusIfAny(statusToFilter) { + const filteredTests = this.props.tests.filter((test) => { + return (test.status === statusToFilter); + } + ); + + if (filteredTests.length > 0) { + const listItems = filteredTests.map((test) => { + return (
  • {test.test}
  • ) + }); + return + +
      {listItems}
    +
    ; + } + return ; + } +} + +export class PrinciplesStatusTable extends AuthComponent { + render() { + return ; + } +} + +export default PrinciplesStatusTable; + +PrinciplesStatusTable.propTypes = {principlesStatus: PropTypes.array}; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/ReportLegend.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/ReportLegend.js new file mode 100644 index 000000000..5ef75f2b4 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/ReportLegend.js @@ -0,0 +1,60 @@ +import React, {Component} from "react"; +import StatusLabel from "./StatusLabel"; +import {ZeroTrustStatuses} from "./ZeroTrustPillars"; +import {NavLink} from "react-router-dom"; +import {Panel} from "react-bootstrap"; + + +class ZeroTrustReportLegend extends Component { + render() { + const legendContent = this.getLegendContent(); + + return ( + + + +

    Legend

    +
    +
    + + + {legendContent} + + +
    + ); + } + + getLegendContent() { + return
    +
      +
    • +
      + +
      + {"\t"}At least one of the tests related to this component failed. This means that the Infection Monkey detected an unmet Zero Trust requirement. +
    • +
    • +
      + +
      + {"\t"}At least one of the tests’ results related to this component requires further manual verification. +
    • +
    • +
      + +
      + {"\t"}All Tests related to this pillar passed. No violation of a Zero Trust guiding principle was detected. +
    • +
    • +
      + +
      + {"\t"}This status means the test wasn't executed.To activate more tests, refer to the Monkey configuration page. +
    • +
    +
    ; + } +} + +export default ZeroTrustReportLegend; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/SinglePillarPrinciplesStatus.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/SinglePillarPrinciplesStatus.js new file mode 100644 index 000000000..8e4512ac7 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/SinglePillarPrinciplesStatus.js @@ -0,0 +1,37 @@ +import AuthComponent from "../../AuthComponent"; +import PillarLabel from "./PillarLabel"; +import PrinciplesStatusTable from "./PrinciplesStatusTable"; +import React from "react"; +import * as PropTypes from "prop-types"; +import {Panel} from "react-bootstrap"; + +export default class SinglePillarPrinciplesStatus extends AuthComponent { + render() { + if (this.props.principlesStatus.length === 0) { + return null; + } + else { + return ( + + + +

    + +

    +
    +
    + + + + + +
    + ); + } + } +} + +SinglePillarPrinciplesStatus.propTypes = { + principlesStatus: PropTypes.array, + pillar: PropTypes.string, +}; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/StatusLabel.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/StatusLabel.js new file mode 100644 index 000000000..028ca7d89 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/StatusLabel.js @@ -0,0 +1,37 @@ +import React, {Component} from "react"; +import * as PropTypes from "prop-types"; + +const statusToIcon = { + "Passed": "fa-check", + "Verify": "fa-exclamation-triangle", + "Failed": "fa-bomb", + "Unexecuted": "fa-question", +}; + +export const statusToLabelType = { + "Passed": "label-success", + "Verify": "label-warning", + "Failed": "label-danger", + "Unexecuted": "label-default", +}; + +export default class StatusLabel extends Component { + render() { + let text = ""; + if (this.props.showText) { + text = " " + this.props.status; + } + + return ( +
    + {text} +
    + ); + } +} + +StatusLabel.propTypes = { + status: PropTypes.string, + showText: PropTypes.bool, + size: PropTypes.string +}; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/StatusesToPillarsSummary.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/StatusesToPillarsSummary.js new file mode 100644 index 000000000..d34a484b9 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/StatusesToPillarsSummary.js @@ -0,0 +1,37 @@ +import React, {Component, Fragment} from "react"; +import PillarLabel from "./PillarLabel"; +import StatusLabel from "./StatusLabel"; +import * as PropTypes from "prop-types"; +import {ZeroTrustStatuses} from "./ZeroTrustPillars"; + +export default class StatusesToPillarsSummary extends Component { + render() { + return (
    + {this.getStatusSummary(ZeroTrustStatuses.failed)} + {this.getStatusSummary(ZeroTrustStatuses.verify)} + {this.getStatusSummary(ZeroTrustStatuses.passed)} + {this.getStatusSummary(ZeroTrustStatuses.unexecuted)} +
    ); + } + + getStatusSummary(status) { + if (this.props.statusesToPillars[status].length > 0) { + return +

    + +

    +
    + { + this.props.statusesToPillars[status].map((pillar) => { + return + }) + } +
    +
    + } + } +} + +StatusesToPillarsSummary.propTypes = { + statusesToPillars: PropTypes.object +}; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/SummarySection.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/SummarySection.js new file mode 100644 index 000000000..e4012bf50 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/SummarySection.js @@ -0,0 +1,40 @@ +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
    +

    Summary

    + + + + +

    + Get a quick glance at how your network aligns with the + Zero Trust eXtended (ZTX) framework + . +

    + +
    + + + + + + + + +
    +
    + } +} + +SummarySection.propTypes = { + allMonkeysAreDead: PropTypes.bool, + pillars: PropTypes.object +}; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/ZeroTrustPillars.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/ZeroTrustPillars.js new file mode 100644 index 000000000..dd2a55865 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/ZeroTrustPillars.js @@ -0,0 +1,18 @@ +export const ZeroTrustPillars = { + data: "Data", + people: "People", + network: "Networks", + workload: "Workload", + devices: "Devices", + visibility: "Visibility & Analytics", + automation: "Automation & Orchestration" +}; + +export const ZeroTrustStatuses = { + failed: "Failed", + verify: "Verify", + passed: "Passed", + unexecuted: "Unexecuted" +}; + +export default ZeroTrustPillars; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/.DS_Store b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/.DS_Store new file mode 100644 index 000000000..344923cf9 Binary files /dev/null and b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/.DS_Store differ diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/ArcNode.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/ArcNode.js new file mode 100644 index 000000000..aee1fb7f2 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/ArcNode.js @@ -0,0 +1,65 @@ +import React from 'react' +import PropTypes from 'prop-types'; +import {Popover, OverlayTrigger} from 'react-bootstrap'; +import * as d3 from 'd3' + +class ArcNode extends React.Component { + render() { + let {prefix, index, data} = this.props; + + let arc = d3.arc().innerRadius(data.inner).outerRadius(data.outer).startAngle(0).endAngle(Math.PI * 2.0); + let id = prefix + 'Node_' + index; + + return ( + + {data.tooltip}} rootClose> + + + + + {data.icon + '\u2000'} + {data.label} + + + + ); + } + + + handleClick(e_) { + this.props.disableHover(this.refs.overlay); + } + + handleOver(e_) { + if (this.props.hover) { + this.refs.overlay.show(); + } + } + + handleOut(e_) { + if (this.props.hover) { + this.refs.overlay.hide(); + } + } +} + +ArcNode.propTypes = { + data: PropTypes.object +}; + +export default ArcNode; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/CircularNode.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/CircularNode.js new file mode 100644 index 000000000..5c84d95a5 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/CircularNode.js @@ -0,0 +1,64 @@ +import React from 'react' +import PillarLabel from "../PillarLabel"; +import {Popover, OverlayTrigger} from 'react-bootstrap'; +import PropTypes from 'prop-types'; + +class CircularNode extends React.Component { + render() { + let {prefix, index, data} = this.props; + + let translate = 'translate(' + data.cx + ',' + data.cy + ')'; + return ( + + {data.tooltip}} rootClose> + + + + + + + ); + } + + + handleClick(e_) { + this.props.disableHover(this.refs.overlay); + } + + handleOver(e_) { + if (this.props.hover) { + this.refs.overlay.show(); + } + } + + handleOut(e_) { + if (this.props.hover) { + this.refs.overlay.hide(); + } + } + +} + +CircularNode.propTypes = { + index: PropTypes.number, + data: PropTypes.object +}; + +export default CircularNode; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/ResponsiveVennDiagram.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/ResponsiveVennDiagram.js new file mode 100644 index 000000000..4b2069f06 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/ResponsiveVennDiagram.js @@ -0,0 +1,28 @@ +import React from 'react' +import PropTypes from 'prop-types' +import Dimensions from 'react-dimensions' +import VennDiagram from './VennDiagram' + +const VENN_MIN_WIDTH = '300px'; + +class ResponsiveVennDiagram extends React.Component { + constructor(props) { + super(props); + } + + render() { + const {pillarsGrades} = this.props; + + return ( +
    + +
    + ); + } +} + +ResponsiveVennDiagram.propTypes = { + pillarsGrades: PropTypes.array +}; + +export default Dimensions()(ResponsiveVennDiagram); diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/Utility.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/Utility.js new file mode 100644 index 000000000..fa9309506 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/Utility.js @@ -0,0 +1,9 @@ +export class TypographicUtilities { + static removeAmpersand(string_) { + return string_.replace(' & ', 'And'); + } + + static removeBrokenBar(string_) { + return string_.replace(/\|/g, ' '); + } +} diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/VennDiagram.css b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/VennDiagram.css new file mode 100644 index 000000000..dd4883125 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/VennDiagram.css @@ -0,0 +1,17 @@ +@import url('https://fonts.googleapis.com/css?family=Noto+Sans&display=swap'); + +body { + margin: 0; + font-family: "Noto Sans", sans-serif; +} + +svg { + + -webkit-touch-callout: none; /* iOS Safari */ + -webkit-user-select: none; /* Safari */ + -khtml-user-select: none; /* Konqueror HTML */ + -moz-user-select: none; /* Firefox */ + -ms-user-select: none; /* Internet Explorer/Edge */ + user-select: none; /* Non-prefixed version, currently supported by Chrome and Opera */ + +} diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/VennDiagram.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/VennDiagram.js new file mode 100644 index 000000000..70304daad --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/VennDiagram.js @@ -0,0 +1,280 @@ +import React from 'react' +import PropTypes from 'prop-types' +import CircularNode from './CircularNode' +import ArcNode from './ArcNode' +import {TypographicUtilities} from './Utility.js' +import './VennDiagram.css' +import {ZeroTrustStatuses} from "../ZeroTrustPillars"; + +class VennDiagram extends React.Component { + constructor(props_) { + super(props_); + + this.state = {hover: true, currentPopover: undefined}; + this._disableHover = this._disableHover.bind(this); + + this.width = this.height = 512; + + this.prefix = 'vennDiagram'; + this.fontStyles = [{size: Math.max(9, this.width / 28), color: 'white'}, { + size: Math.max(6, this.width / 38), + color: 'white' + }, {size: Math.max(6, this.width / 48), color: 'white'}]; + this.offset = this.width / 16; + + this.thirdWidth = this.width / 3; + this.width11By2 = this.width / 5.5; + this.width2By7 = 2 * this.width / 7; + this.width1By11 = this.width / 11; + this.width1By28 = this.width / 28; + this.arcNodesGap = 4; + + this.layout = { + Data: {cx: 0, cy: 0, r: this.width11By2, offset: {x: 0, y: 0}, popover: 'top'}, + People: { + cx: -this.width2By7, + cy: 0, + r: this.width11By2, + offset: {x: this.width1By11 + this.fontStyles[1].size / 5 * 3, y: 0}, + popover: 'right' + }, + Networks: { + cx: this.width2By7, + cy: 0, + r: this.width11By2, + offset: {x: -this.width1By11 - this.fontStyles[1].size / 5 * 3, y: 0}, + popover: 'left' + }, + Devices: { + cx: 0, + cy: this.width2By7, + r: this.width11By2, + offset: {x: 0, y: -this.width1By11 + this.fontStyles[1].size / 6 * 3}, + popover: 'top' + }, + Workloads: { + cx: 0, + cy: -this.width2By7, + r: this.width11By2, + offset: {x: 0, y: this.width1By11}, + popover: 'bottom' + }, + VisibilityAndAnalytics: { + inner: this.thirdWidth - this.width1By28, + outer: this.thirdWidth, + icon: '\uf070', + popover: 'right' + }, + AutomationAndOrchestration: { + inner: this.thirdWidth - this.width1By28 * 2 - this.arcNodesGap, + outer: this.thirdWidth - this.width1By28 - this.arcNodesGap, + icon: '\uf085', + popover: 'right' + } + }; + + /* + + RULE #1: All scores have to be equal 0, except Unexecuted [U] which could be also a negative integer + sum(C, I, P) has to be <=0 + + RULE #2: Failed [C] has to be > 0, + sum(C) > 0 + + RULE #3: Verify [I] has to be > 0 while Failed has to be 0, + sum(C, I) > 0 and C * I = 0, while C has to be 0 + + RULE #4: By process of elimination, passed. + if the P is bigger by 2 then negative U, first conditional + would be true. + */ + + this.rules = [ + + { + id: 'Rule #1', status: ZeroTrustStatuses.unexecuted, hex: '#777777', f: function (d_) { + return d_[ZeroTrustStatuses.failed] + d_[ZeroTrustStatuses.verify] + d_[ZeroTrustStatuses.passed] === 0; + } + }, + { + id: 'Rule #2', status: ZeroTrustStatuses.failed, hex: '#D9534F', f: function (d_) { + return d_[ZeroTrustStatuses.failed] > 0; + } + }, + { + id: 'Rule #3', status: ZeroTrustStatuses.verify, hex: '#F0AD4E', f: function (d_) { + return d_[ZeroTrustStatuses.failed] === 0 && d_[ZeroTrustStatuses.verify] > 0; + } + }, + { + id: 'Rule #4', status: ZeroTrustStatuses.passed, hex: '#5CB85C', f: function (d_) { + return d_[ZeroTrustStatuses.passed] > 0; + } + } + + ]; + + } + + componentDidMount() { + this.parseData(); + if (this.state.currentPopover !== undefined) { + this.state.currentPopover.show(); + } + } + + _disableHover(ref_) { + this.setState({hover: false, currentPopover: ref_, data: this.state.data}); + } + + _onMouseMove(e) { + + let self = this; + + let hidden = 'none'; + let html = ''; + let bcolor = '#DEDEDE'; + + if (this.state.currentPopover !== undefined) { + this.state.currentPopover.show(); + } + + document.querySelectorAll('circle, path').forEach((d_, i_) => { + d_.setAttribute('opacity', "0.8"); + }); + + if (e.target.id.includes('Node')) { + + e.target.setAttribute('opacity', 0.95); + + // Set highest z-index + e.target.parentNode.parentNode.appendChild(e.target.parentNode); + + } else { + + // Return z indices to default + Object.keys(this.layout).forEach(function (d_, i_) { + document.querySelector('#' + self.prefix).appendChild(document.querySelector('#' + self.prefix + 'Node_' + i_).parentNode); + }) + } + + } + + _onClick(e) { + + if (!e.target.id.includes('Node')) { + + this.state.currentPopover.hide(); + this.setState({hover: true, currentPopover: undefined, data: this.state.data}); + } + } + + parseData() { + + let self = this; + let data = []; + const omit = (prop, {[prop]: _, ...rest}) => rest; + + this.props.pillarsGrades.forEach((d_, i_) => { + + let params = omit('pillar', d_); + let sum = Object.keys(params).reduce((sum_, key_) => sum_ + parseFloat(params[key_] || 0), 0); + let key = TypographicUtilities.removeAmpersand(d_.pillar); + let html = self.buildTooltipHtmlContent(params); + let rule = null; + + for (let j = 0; j < self.rules.length; j++) { + if (self.rules[j].f(d_)) { + rule = j; + break; + } + } + + self.setLayoutElement(rule, key, html, d_); + data.push(this.layout[key]); + + }); + + this.setState({hover: true, activePopover: undefined, data: data}); + this.render(); + } + + buildTooltipHtmlContent(object_) { + + return Object.keys(object_).map((key_, i_) => { + return (

    {key_}: {object_[key_]}

    ) + }) + } + + setLayoutElement(rule_, key_, html_, d_) { + + if (rule_ === null) { + console.log(Error('The node scores are invalid, please check the data or the rules set.')); + } + + if (key_ === 'Data') { + this.layout[key_].fontStyle = this.fontStyles[0]; + } else if (this.layout[key_].hasOwnProperty('cx')) { + this.layout[key_].fontStyle = this.fontStyles[1]; + } else { + this.layout[key_].fontStyle = this.fontStyles[2]; + } + + this.layout[key_].hex = this.rules[rule_].hex; + this.layout[key_].status = this.rules[rule_].status; + this.layout[key_].label = d_.pillar; + this.layout[key_].node = d_; + this.layout[key_].tooltip = html_; + } + + render() { + if (this.state.data === undefined) { + return null; + } else { + // equivalent to center translate (width/2, height/2) + let viewPortParameters = (-this.width / 2) + ' ' + (-this.height / 2) + ' ' + this.width + ' ' + this.height; + let nodes = Object.values(this.layout).map((d_, i_) => { + if (d_.hasOwnProperty('cx')) { + return ( + + ); + } else { + d_.label = TypographicUtilities.removeBrokenBar(d_.label); + return ( + + ); + } + }); + + return ( +
    this.divElement = divElement} onMouseMove={this._onMouseMove.bind(this)} + onClick={this._onClick.bind(this)}> + + {nodes} + +
    + ) + } + } +} + +VennDiagram.propTypes = { + pillarsGrades: PropTypes.array +}; + +export default VennDiagram; diff --git a/monkey/monkey_island/cc/ui/src/components/utils/SaveJsonToFile.js b/monkey/monkey_island/cc/ui/src/components/utils/SaveJsonToFile.js new file mode 100644 index 000000000..6ad124457 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/utils/SaveJsonToFile.js @@ -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"); +} diff --git a/monkey/monkey_island/cc/ui/src/images/zerotrust/im-alert-machine-icon.svg b/monkey/monkey_island/cc/ui/src/images/zerotrust/im-alert-machine-icon.svg new file mode 100644 index 000000000..507541be4 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/images/zerotrust/im-alert-machine-icon.svg @@ -0,0 +1 @@ +im-alert-machine-icon \ No newline at end of file diff --git a/monkey/monkey_island/cc/ui/src/images/zerotrust/im-alert-network-icon.svg b/monkey/monkey_island/cc/ui/src/images/zerotrust/im-alert-network-icon.svg new file mode 100644 index 000000000..50dcc6726 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/images/zerotrust/im-alert-network-icon.svg @@ -0,0 +1 @@ +im-alert-network-icon \ No newline at end of file diff --git a/monkey/monkey_island/cc/ui/src/styles/App.css b/monkey/monkey_island/cc/ui/src/styles/App.css index 8f602fa08..109f1c147 100644 --- a/monkey/monkey_island/cc/ui/src/styles/App.css +++ b/monkey/monkey_island/cc/ui/src/styles/App.css @@ -549,6 +549,22 @@ body { color: #e0ddde !important; } +.status-success { + color: #24b716 !important; +} + +.status-warning { + color: #b1a91c !important; +} + +.status-danger { + color: #d91016 !important; +} + +.status-default { + color: #575556 !important; +} + .attack-legend { text-align: center; margin-bottom: 20px;